cli-agent-runner 0.2.6__tar.gz → 0.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/.vulture-whitelist.py +18 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/CHANGELOG.md +12 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/PKG-INFO +2 -2
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/_docgen.py +2 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/_emit.py +24 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/api.py +3 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/api_types.py +1 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/cli/serve_cmd.py +72 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/config.py +43 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/events.py +2 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/monitor.py +52 -1
- cli_agent_runner-0.2.7/agent_runner/schedule.py +178 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/architecture.md +2 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/configuration.md +80 -0
- cli_agent_runner-0.2.7/docs/migrations/0.2.7.md +121 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/runbook.md +47 -1
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/_test_helpers.py +2 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_architecture.py +3 -0
- cli_agent_runner-0.2.7/tests/unit/test_config_schedule.py +65 -0
- cli_agent_runner-0.2.7/tests/unit/test_emit_schedule.py +32 -0
- cli_agent_runner-0.2.7/tests/unit/test_monitor_schedule_pause.py +103 -0
- cli_agent_runner-0.2.7/tests/unit/test_peek_schedule.py +35 -0
- cli_agent_runner-0.2.7/tests/unit/test_schedule.py +183 -0
- cli_agent_runner-0.2.7/tests/unit/test_serve_schedule_gate.py +143 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/.gitignore +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/BACKLOG.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/LICENSE +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/README.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/README.zh.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/SECURITY.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/agent_runtime.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/pi.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/runner.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/build.sh +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/README.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/commands.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/events.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.33.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.34.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.35.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.36.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.37.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.38.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.39.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.40.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.1.42.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.2.0.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.2.1.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.2.2.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.2.3.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.2.4.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.2.5.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/migrations/0.2.6.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/plugins.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/pyproject.toml +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_events.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_events_cmd_tail.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.2.6 → cli_agent_runner-0.2.7}/tests/unit/test_vcs_state.py +0 -0
|
@@ -50,6 +50,7 @@ service
|
|
|
50
50
|
recent_events
|
|
51
51
|
recent_hook_failures
|
|
52
52
|
recent_blips
|
|
53
|
+
schedule
|
|
53
54
|
|
|
54
55
|
# agent_runner.api_types.RateLimitState
|
|
55
56
|
throttled_until_epoch
|
|
@@ -121,6 +122,7 @@ vcs
|
|
|
121
122
|
monitor
|
|
122
123
|
phases
|
|
123
124
|
plugins
|
|
125
|
+
schedule
|
|
124
126
|
|
|
125
127
|
# agent_runner.config.MonitorConfig
|
|
126
128
|
auth_fail_patterns
|
|
@@ -176,6 +178,11 @@ dry_run
|
|
|
176
178
|
max_grace_after_result_s
|
|
177
179
|
grace_kill_ignore_patterns
|
|
178
180
|
|
|
181
|
+
# agent_runner.config.ScheduleConfig
|
|
182
|
+
timezone
|
|
183
|
+
run_windows
|
|
184
|
+
pause_windows
|
|
185
|
+
|
|
179
186
|
# agent_runner.config.VcsConfig
|
|
180
187
|
stash_idempotency_s
|
|
181
188
|
dirty_action
|
|
@@ -233,6 +240,17 @@ deleted
|
|
|
233
240
|
deferred
|
|
234
241
|
existing
|
|
235
242
|
|
|
243
|
+
# agent_runner.schedule.PauseDecision
|
|
244
|
+
paused
|
|
245
|
+
resume_at
|
|
246
|
+
active_window
|
|
247
|
+
|
|
248
|
+
# agent_runner.schedule.Window
|
|
249
|
+
start_min
|
|
250
|
+
end_min
|
|
251
|
+
label
|
|
252
|
+
days
|
|
253
|
+
|
|
236
254
|
# agent_runner.startup_check.CheckResult
|
|
237
255
|
name
|
|
238
256
|
ok
|
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.7] - 2026-08-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `[schedule]` config gates the serve loop between rounds: `pause_windows` / `run_windows` decide whether the next round launches now or the supervisor idle-sleeps until a window opens, then auto-resumes. Windows are `[WEEKDAYS ]HH:MM-HH:MM` (24h, end-exclusive, midnight-wrap; optional `Mon-Fri` / `Sat,Sun` weekday prefix, bare = every day). Timezone-aware (`timezone`, IANA; omit → host local). Evaluation is `run AND NOT pause`. Absent section → unchanged 7×24 behavior. An in-flight round is never interrupted; `agent-runner round` is never gated.
|
|
12
|
+
- `schedule_paused` / `schedule_resumed` events; `peek` surfaces the current pause state (`schedule` field).
|
|
13
|
+
- `serve --ignore-schedule` runs rounds regardless of the windows (catch-up / testing).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Monitor no longer raises `supervisor_stale` during an intentional schedule pause (suppressed until `resume_at` plus one staleness window).
|
|
17
|
+
|
|
18
|
+
See `docs/migrations/0.2.7.md`.
|
|
19
|
+
|
|
8
20
|
## [0.2.6] - 2026-07-28
|
|
9
21
|
|
|
10
22
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Restart-on-exit supervisor for autonomous CLI agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/wan9yu/cli-agent-runner
|
|
6
6
|
Project-URL: Documentation, https://github.com/wan9yu/cli-agent-runner#readme
|
|
@@ -22,6 +22,7 @@ from agent_runner.config import (
|
|
|
22
22
|
PluginsConfig,
|
|
23
23
|
PromptConfig,
|
|
24
24
|
RuntimeConfig,
|
|
25
|
+
ScheduleConfig,
|
|
25
26
|
VcsConfig,
|
|
26
27
|
)
|
|
27
28
|
from agent_runner.defenses import catalog
|
|
@@ -45,6 +46,7 @@ _SECTIONS: list[tuple[str, type, list[tuple[str, type]]]] = [
|
|
|
45
46
|
("monitor", MonitorConfig, [("monitor.host_health", MonitorHostHealthConfig)]),
|
|
46
47
|
("phases", PhasesConfig, []),
|
|
47
48
|
("plugins", PluginsConfig, []),
|
|
49
|
+
("schedule", ScheduleConfig, []),
|
|
48
50
|
]
|
|
49
51
|
|
|
50
52
|
|
|
@@ -26,6 +26,8 @@ __all__ = [
|
|
|
26
26
|
"emit_round_progress",
|
|
27
27
|
"emit_round_substrate_after",
|
|
28
28
|
"emit_round_substrate_before",
|
|
29
|
+
"emit_schedule_paused",
|
|
30
|
+
"emit_schedule_resumed",
|
|
29
31
|
"emit_stop_file_detected",
|
|
30
32
|
"emit_transient_error_backoff_capped",
|
|
31
33
|
"emit_transient_error_detected",
|
|
@@ -85,6 +87,28 @@ def emit_stop_file_detected(
|
|
|
85
87
|
)
|
|
86
88
|
|
|
87
89
|
|
|
90
|
+
def emit_schedule_paused(
|
|
91
|
+
log_dir: Path, *, active_window: str, resume_at: str, timezone: str
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Emit schedule_paused when the serve loop enters a configured pause window."""
|
|
94
|
+
from agent_runner.events import SCHEDULE_PAUSED, emit
|
|
95
|
+
|
|
96
|
+
emit(
|
|
97
|
+
log_dir,
|
|
98
|
+
SCHEDULE_PAUSED,
|
|
99
|
+
active_window=active_window,
|
|
100
|
+
resume_at=resume_at,
|
|
101
|
+
timezone=timezone,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def emit_schedule_resumed(log_dir: Path, *, paused_for_s: int) -> None:
|
|
106
|
+
"""Emit schedule_resumed when the serve loop exits a pause window."""
|
|
107
|
+
from agent_runner.events import SCHEDULE_RESUMED, emit
|
|
108
|
+
|
|
109
|
+
emit(log_dir, SCHEDULE_RESUMED, paused_for_s=paused_for_s)
|
|
110
|
+
|
|
111
|
+
|
|
88
112
|
def emit_round_substrate_before(
|
|
89
113
|
log_dir: Path, *, round_num: int, git_head: str | None, paths_hash: str | None
|
|
90
114
|
) -> None:
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.7'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 7)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -473,6 +473,7 @@ def peek(
|
|
|
473
473
|
recent_events=recent,
|
|
474
474
|
recent_hook_failures=recent_hook_failures,
|
|
475
475
|
recent_blips=recent_blips,
|
|
476
|
+
schedule=monitor.latest_schedule_state(parsed_events),
|
|
476
477
|
)
|
|
477
478
|
return state if select is None else select_path(state, select)
|
|
478
479
|
|
|
@@ -746,6 +747,8 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
|
|
|
746
747
|
emit_round_progress,
|
|
747
748
|
emit_round_substrate_after,
|
|
748
749
|
emit_round_substrate_before,
|
|
750
|
+
emit_schedule_paused,
|
|
751
|
+
emit_schedule_resumed,
|
|
749
752
|
emit_stop_file_detected,
|
|
750
753
|
emit_transient_error_backoff_capped,
|
|
751
754
|
emit_transient_error_detected,
|
|
@@ -98,6 +98,7 @@ class ProjectState:
|
|
|
98
98
|
recent_events: list[dict[str, Any]] = field(default_factory=list)
|
|
99
99
|
recent_hook_failures: list[dict[str, Any]] = field(default_factory=list)
|
|
100
100
|
recent_blips: list[dict[str, Any]] = field(default_factory=list)
|
|
101
|
+
schedule: dict | None = None
|
|
101
102
|
|
|
102
103
|
|
|
103
104
|
@dataclass(frozen=True)
|
|
@@ -17,6 +17,7 @@ import sys
|
|
|
17
17
|
import time
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
|
+
from agent_runner import schedule
|
|
20
21
|
from agent_runner._substrate import compute_git_head, compute_paths_hash
|
|
21
22
|
from agent_runner._throttle import _check_throttle_state
|
|
22
23
|
from agent_runner._throttle import reset_counters as _reset_counters
|
|
@@ -30,6 +31,8 @@ from agent_runner.api import (
|
|
|
30
31
|
emit_round_logs_prune_deferred,
|
|
31
32
|
emit_round_substrate_after,
|
|
32
33
|
emit_round_substrate_before,
|
|
34
|
+
emit_schedule_paused,
|
|
35
|
+
emit_schedule_resumed,
|
|
33
36
|
emit_stop_file_detected,
|
|
34
37
|
post_round_decision,
|
|
35
38
|
)
|
|
@@ -58,6 +61,68 @@ def _resolve_max_rounds(*, cli_value: int | None, config_value: int | None) -> i
|
|
|
58
61
|
return effective
|
|
59
62
|
|
|
60
63
|
|
|
64
|
+
def _maybe_pause_for_schedule(
|
|
65
|
+
cfg,
|
|
66
|
+
log_dir,
|
|
67
|
+
stop,
|
|
68
|
+
*,
|
|
69
|
+
now_fn=schedule.now_in_zone,
|
|
70
|
+
sleep_fn=time.sleep,
|
|
71
|
+
chunk_s: int = 30,
|
|
72
|
+
) -> bool:
|
|
73
|
+
"""If the current time is outside the run schedule, pause until it opens.
|
|
74
|
+
|
|
75
|
+
Returns True if a pause was entered (caller should ``continue`` so the
|
|
76
|
+
top-of-loop guards re-run), False if runnable now. During a pause, sleeps
|
|
77
|
+
in <= chunk_s slices so SIGTERM / SIGINT (which set stop["requested"]) lands
|
|
78
|
+
within one slice.
|
|
79
|
+
|
|
80
|
+
We do NOT check the self-terminate sentinel here: no round runs during a
|
|
81
|
+
pause, so no new sentinel can appear, and any pre-existing one already broke
|
|
82
|
+
the loop at its top before this gate. schedule_resumed is emitted ONLY when
|
|
83
|
+
the window actually opens — an interrupted pause (stop) is a termination, not
|
|
84
|
+
a resume, and correctly leaves schedule_paused as the newest event."""
|
|
85
|
+
sched = cfg.schedule
|
|
86
|
+
if not sched.enabled:
|
|
87
|
+
return False
|
|
88
|
+
decision = schedule.evaluate(
|
|
89
|
+
run_windows=sched.run_windows,
|
|
90
|
+
pause_windows=sched.pause_windows,
|
|
91
|
+
now_local=now_fn(sched.timezone),
|
|
92
|
+
)
|
|
93
|
+
if not decision.paused:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
started = time.monotonic()
|
|
97
|
+
emit_schedule_paused(
|
|
98
|
+
log_dir,
|
|
99
|
+
active_window=decision.active_window or "",
|
|
100
|
+
resume_at=decision.resume_at.isoformat() if decision.resume_at else "",
|
|
101
|
+
timezone=sched.timezone or "local",
|
|
102
|
+
)
|
|
103
|
+
stop_file = cfg.runtime.stop_file
|
|
104
|
+
window_opened = False
|
|
105
|
+
while not stop["requested"]:
|
|
106
|
+
if stop_file is not None and stop_file.exists():
|
|
107
|
+
# Operator stop_file dropped mid-pause: break without emitting
|
|
108
|
+
# schedule_resumed (the window did not open). The serve loop's
|
|
109
|
+
# top-of-loop stop_file check then emits stop_file_detected and exits.
|
|
110
|
+
break
|
|
111
|
+
# #3: should_run (not evaluate) avoids the 8-day next_resume_at scan on
|
|
112
|
+
# every poll; should_run is the negation of the pre-loop paused decision.
|
|
113
|
+
if schedule.should_run(
|
|
114
|
+
now_fn(sched.timezone),
|
|
115
|
+
run_windows=sched.run_windows,
|
|
116
|
+
pause_windows=sched.pause_windows,
|
|
117
|
+
):
|
|
118
|
+
window_opened = True
|
|
119
|
+
break
|
|
120
|
+
sleep_fn(chunk_s)
|
|
121
|
+
if window_opened:
|
|
122
|
+
emit_schedule_resumed(log_dir, paused_for_s=int(time.monotonic() - started))
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
|
|
61
126
|
def _prune_serve_round_logs(log_dir: Path, retention: int) -> None:
|
|
62
127
|
"""Prune the serve-level ``round-<N>.log`` family; report a deferred prune.
|
|
63
128
|
|
|
@@ -101,6 +166,11 @@ def add_parser(sub, parent) -> None:
|
|
|
101
166
|
p = sub.add_parser("serve", parents=[parent], help="Long-running supervisor loop")
|
|
102
167
|
p.add_argument("--once", action="store_true", help="Run a single round then exit (debug)")
|
|
103
168
|
_add_max_rounds_arg(p)
|
|
169
|
+
p.add_argument(
|
|
170
|
+
"--ignore-schedule",
|
|
171
|
+
action="store_true",
|
|
172
|
+
help="Run rounds regardless of [schedule] pause/run windows (testing / catch-up)",
|
|
173
|
+
)
|
|
104
174
|
p.set_defaults(func=cmd)
|
|
105
175
|
|
|
106
176
|
|
|
@@ -177,6 +247,8 @@ def cmd(args) -> int:
|
|
|
177
247
|
max_rounds=effective_max_rounds,
|
|
178
248
|
)
|
|
179
249
|
break
|
|
250
|
+
if not args.ignore_schedule and _maybe_pause_for_schedule(cfg, log_dir, stop):
|
|
251
|
+
continue
|
|
180
252
|
round_num = next_round_num(log_dir)
|
|
181
253
|
git_head_before = compute_git_head(work_dir)
|
|
182
254
|
paths_hash_before = compute_paths_hash(
|
|
@@ -8,6 +8,8 @@ from dataclasses import dataclass, field
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any, Literal
|
|
10
10
|
|
|
11
|
+
from agent_runner import schedule
|
|
12
|
+
|
|
11
13
|
_VALID_INJECTION_MODES: frozenset[str] = frozenset({"prepend", "file", "none"})
|
|
12
14
|
_VALID_DIRTY_ACTIONS: frozenset[str] = frozenset({"stash", "ignore", "auto_commit"})
|
|
13
15
|
_VALID_TRANSIENT_ERROR_ACTIONS: frozenset[str] = frozenset({"back_off", "skip", "stop"})
|
|
@@ -168,6 +170,17 @@ class MonitorConfig:
|
|
|
168
170
|
"""
|
|
169
171
|
|
|
170
172
|
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class ScheduleConfig:
|
|
175
|
+
timezone: str | None = None # IANA name; None = host local time
|
|
176
|
+
run_windows: tuple[schedule.Window, ...] = ()
|
|
177
|
+
pause_windows: tuple[schedule.Window, ...] = ()
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def enabled(self) -> bool:
|
|
181
|
+
return bool(self.run_windows or self.pause_windows)
|
|
182
|
+
|
|
183
|
+
|
|
171
184
|
@dataclass(frozen=True)
|
|
172
185
|
class Config:
|
|
173
186
|
agent: AgentConfig
|
|
@@ -177,6 +190,7 @@ class Config:
|
|
|
177
190
|
monitor: MonitorConfig = field(default_factory=MonitorConfig)
|
|
178
191
|
phases: PhasesConfig = field(default_factory=PhasesConfig)
|
|
179
192
|
plugins: PluginsConfig = field(default_factory=PluginsConfig)
|
|
193
|
+
schedule: ScheduleConfig = field(default_factory=ScheduleConfig)
|
|
180
194
|
|
|
181
195
|
|
|
182
196
|
def _require(d: dict, *path: str) -> object:
|
|
@@ -357,6 +371,32 @@ def _parse_fresh_eyes_every_n(runtime_d: dict) -> int | None:
|
|
|
357
371
|
return _require_positive_int(raw, field="runtime.fresh_eyes_every_n")
|
|
358
372
|
|
|
359
373
|
|
|
374
|
+
def _parse_schedule(schedule_d: dict) -> ScheduleConfig:
|
|
375
|
+
tz = schedule_d.get("timezone")
|
|
376
|
+
if tz is not None:
|
|
377
|
+
tz = str(tz)
|
|
378
|
+
if not schedule.valid_timezone(tz):
|
|
379
|
+
raise ConfigError(f"schedule.timezone: unknown IANA zone {tz!r}")
|
|
380
|
+
|
|
381
|
+
def _parse_list(key: str) -> tuple[schedule.Window, ...]:
|
|
382
|
+
raw = schedule_d.get(key, [])
|
|
383
|
+
if not isinstance(raw, list):
|
|
384
|
+
raise ConfigError(f"schedule.{key} must be a list of window strings")
|
|
385
|
+
out = []
|
|
386
|
+
for w in raw:
|
|
387
|
+
try:
|
|
388
|
+
out.append(schedule.parse_window(str(w)))
|
|
389
|
+
except ValueError as e:
|
|
390
|
+
raise ConfigError(f"schedule.{key}: {e}") from e
|
|
391
|
+
return tuple(out)
|
|
392
|
+
|
|
393
|
+
return ScheduleConfig(
|
|
394
|
+
timezone=tz,
|
|
395
|
+
run_windows=_parse_list("run_windows"),
|
|
396
|
+
pause_windows=_parse_list("pause_windows"),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
360
400
|
def load_config(toml_path: Path) -> Config:
|
|
361
401
|
if not toml_path.exists():
|
|
362
402
|
raise FileNotFoundError(f"config not found: {toml_path}")
|
|
@@ -571,6 +611,8 @@ def load_config(toml_path: Path) -> Config:
|
|
|
571
611
|
disable = list(plugins_raw.pop("disable", []))
|
|
572
612
|
plugins = PluginsConfig(disable=disable, raw=plugins_raw)
|
|
573
613
|
|
|
614
|
+
schedule_cfg = _parse_schedule(raw.get("schedule", {}))
|
|
615
|
+
|
|
574
616
|
cfg = Config(
|
|
575
617
|
agent=agent,
|
|
576
618
|
runtime=runtime,
|
|
@@ -579,6 +621,7 @@ def load_config(toml_path: Path) -> Config:
|
|
|
579
621
|
monitor=monitor,
|
|
580
622
|
phases=phases_cfg,
|
|
581
623
|
plugins=plugins,
|
|
624
|
+
schedule=schedule_cfg,
|
|
582
625
|
)
|
|
583
626
|
|
|
584
627
|
# Honor [plugins] disable — must happen after registries are populated by
|
|
@@ -61,6 +61,8 @@ ROUND_START = "round_start"
|
|
|
61
61
|
ROUND_SUBSTRATE_AFTER = "round_substrate_after"
|
|
62
62
|
ROUND_SUBSTRATE_BEFORE = "round_substrate_before"
|
|
63
63
|
ROUND_TIMEOUT_KILL = "round_timeout_kill"
|
|
64
|
+
SCHEDULE_PAUSED = "schedule_paused"
|
|
65
|
+
SCHEDULE_RESUMED = "schedule_resumed"
|
|
64
66
|
SELF_TERMINATED = "agent_self_terminated"
|
|
65
67
|
SERVE_STARTUP_HOOK_FAILED = "serve_startup_hook_failed"
|
|
66
68
|
SERVICE_UPGRADE_ROLLBACK_FAILED = "service_upgrade_rollback_failed"
|
|
@@ -22,7 +22,7 @@ import time
|
|
|
22
22
|
from collections import deque
|
|
23
23
|
from collections.abc import Iterable
|
|
24
24
|
from dataclasses import dataclass
|
|
25
|
-
from datetime import UTC, datetime
|
|
25
|
+
from datetime import UTC, datetime, timedelta
|
|
26
26
|
from pathlib import Path
|
|
27
27
|
from typing import Any, Protocol
|
|
28
28
|
|
|
@@ -48,6 +48,8 @@ from agent_runner.events import (
|
|
|
48
48
|
ORPHAN_STASHED,
|
|
49
49
|
ROUND_END,
|
|
50
50
|
ROUND_START,
|
|
51
|
+
SCHEDULE_PAUSED,
|
|
52
|
+
SCHEDULE_RESUMED,
|
|
51
53
|
TRANSIENT_ERROR_DETECTED,
|
|
52
54
|
TRANSIENT_ERROR_RECOVERED,
|
|
53
55
|
now_iso_ms,
|
|
@@ -447,6 +449,16 @@ def detect_anomaly_repetitive_active(
|
|
|
447
449
|
)
|
|
448
450
|
|
|
449
451
|
|
|
452
|
+
def _latest_schedule_event(events: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
453
|
+
"""Newest schedule_paused/schedule_resumed event by stream order, or None.
|
|
454
|
+
|
|
455
|
+
Reverse-walk with early break — events grows unboundedly over a project's life."""
|
|
456
|
+
for e in reversed(events):
|
|
457
|
+
if e.get("event") in (SCHEDULE_PAUSED, SCHEDULE_RESUMED):
|
|
458
|
+
return e
|
|
459
|
+
return None
|
|
460
|
+
|
|
461
|
+
|
|
450
462
|
def detect_supervisor_stale(
|
|
451
463
|
events: list[dict[str, Any]],
|
|
452
464
|
*,
|
|
@@ -471,6 +483,30 @@ def detect_supervisor_stale(
|
|
|
471
483
|
age_s = (now - parse_iso_ms(last_ts_str)).total_seconds()
|
|
472
484
|
if age_s <= stale_threshold_s:
|
|
473
485
|
return None
|
|
486
|
+
|
|
487
|
+
newest = _latest_schedule_event(events)
|
|
488
|
+
if newest is not None and newest.get("event") == SCHEDULE_PAUSED:
|
|
489
|
+
# Suppress only while the pause is plausibly still live: until its
|
|
490
|
+
# announced resume_at plus one staleness window. A supervisor that died
|
|
491
|
+
# mid-pause therefore still alarms once that bound passes.
|
|
492
|
+
paused = newest
|
|
493
|
+
bound = None
|
|
494
|
+
try:
|
|
495
|
+
resume_dt = parse_iso_ms(paused.get("resume_at") or "")
|
|
496
|
+
if resume_dt.tzinfo is not None:
|
|
497
|
+
bound = resume_dt
|
|
498
|
+
except (ValueError, TypeError):
|
|
499
|
+
bound = None
|
|
500
|
+
if bound is None:
|
|
501
|
+
# No usable resume_at (empty/unparseable/naive) → bound by the pause's
|
|
502
|
+
# own timestamp + the 8-day resume horizon, so an always-paused config
|
|
503
|
+
# is not a permanent silent-death blind spot.
|
|
504
|
+
try:
|
|
505
|
+
bound = parse_iso_ms(paused["ts"]) + timedelta(days=8)
|
|
506
|
+
except (KeyError, ValueError, TypeError):
|
|
507
|
+
return None # truly no anchor → suppress conservatively
|
|
508
|
+
if now <= bound + timedelta(seconds=stale_threshold_s):
|
|
509
|
+
return None
|
|
474
510
|
return _alert(
|
|
475
511
|
"supervisor_stale",
|
|
476
512
|
"warning",
|
|
@@ -480,6 +516,21 @@ def detect_supervisor_stale(
|
|
|
480
516
|
)
|
|
481
517
|
|
|
482
518
|
|
|
519
|
+
def latest_schedule_state(events: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
520
|
+
"""Current schedule-pause state derived from the event stream.
|
|
521
|
+
|
|
522
|
+
Returns None when there is no pause in effect (no schedule events, or the
|
|
523
|
+
newest one is a resume). When paused, returns the paused indicator dict."""
|
|
524
|
+
newest = _latest_schedule_event(events)
|
|
525
|
+
if newest is None or newest.get("event") != SCHEDULE_PAUSED:
|
|
526
|
+
return None
|
|
527
|
+
return {
|
|
528
|
+
"paused": True,
|
|
529
|
+
"resume_at": newest.get("resume_at", ""),
|
|
530
|
+
"active_window": newest.get("active_window", ""),
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
|
|
483
534
|
# ---------------------------------------------------------------------------
|
|
484
535
|
# State-tree assembly (Task 3.2)
|
|
485
536
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Time-window scheduling core (pure). The serve loop uses this to decide,
|
|
2
|
+
between rounds, whether to run the next round now or idle-sleep until a
|
|
3
|
+
configured window opens. All functions are clock-injectable for testing."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
12
|
+
|
|
13
|
+
_WINDOW_RE = re.compile(r"^(\d{2}):(\d{2})-(\d{2}):(\d{2})$")
|
|
14
|
+
_MINUTES_PER_DAY = 24 * 60
|
|
15
|
+
_WEEKDAYS = {"mon": 0, "tue": 1, "wed": 2, "thu": 3, "fri": 4, "sat": 5, "sun": 6}
|
|
16
|
+
_SEARCH_HORIZON_MIN = 8 * _MINUTES_PER_DAY # weekday-scoped run_windows can pause a full weekend
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_weekday_spec(spec: str) -> frozenset[int]:
|
|
20
|
+
days: set[int] = set()
|
|
21
|
+
for token in spec.split(","):
|
|
22
|
+
token = token.strip().lower()
|
|
23
|
+
if not token:
|
|
24
|
+
raise ValueError(f"empty weekday token in {spec!r}")
|
|
25
|
+
if "-" in token:
|
|
26
|
+
a, _, b = token.partition("-")
|
|
27
|
+
if a not in _WEEKDAYS or b not in _WEEKDAYS:
|
|
28
|
+
raise ValueError(f"invalid weekday range {token!r}")
|
|
29
|
+
start, end = _WEEKDAYS[a], _WEEKDAYS[b]
|
|
30
|
+
if start > end:
|
|
31
|
+
raise ValueError(f"weekday range must be ascending Mon..Sun: {token!r}")
|
|
32
|
+
days.update(range(start, end + 1))
|
|
33
|
+
elif token not in _WEEKDAYS:
|
|
34
|
+
raise ValueError(f"invalid weekday {token!r}")
|
|
35
|
+
else:
|
|
36
|
+
days.add(_WEEKDAYS[token])
|
|
37
|
+
return frozenset(days)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class Window:
|
|
42
|
+
start_min: int # minutes from midnight, [0, 1440)
|
|
43
|
+
end_min: int # minutes from midnight, (0, 1440]
|
|
44
|
+
label: str # original "[WEEKDAYS ]HH:MM-HH:MM" for event payloads
|
|
45
|
+
days: frozenset[int] = frozenset() # empty = every day
|
|
46
|
+
|
|
47
|
+
def contains(self, weekday: int, minute: int) -> bool:
|
|
48
|
+
wraps = self.start_min >= self.end_min
|
|
49
|
+
if wraps: # spans past midnight
|
|
50
|
+
in_time = minute >= self.start_min or minute < self.end_min
|
|
51
|
+
else:
|
|
52
|
+
in_time = self.start_min <= minute < self.end_min
|
|
53
|
+
if not in_time:
|
|
54
|
+
return False
|
|
55
|
+
if not self.days:
|
|
56
|
+
return True
|
|
57
|
+
start_day = weekday
|
|
58
|
+
if wraps and minute < self.end_min:
|
|
59
|
+
start_day = (weekday - 1) % 7 # wrapped tail belongs to the start day
|
|
60
|
+
return start_day in self.days
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _to_minutes(h: int, m: int, *, allow_24: bool, where: str) -> int:
|
|
64
|
+
if h == 24:
|
|
65
|
+
if not allow_24 or m != 0:
|
|
66
|
+
raise ValueError(f"{where}: 24:00 is only valid as an end bound")
|
|
67
|
+
return _MINUTES_PER_DAY
|
|
68
|
+
if not 0 <= h <= 23:
|
|
69
|
+
raise ValueError(f"{where}: hour {h} out of range")
|
|
70
|
+
if not 0 <= m <= 59:
|
|
71
|
+
raise ValueError(f"{where}: minute {m} out of range")
|
|
72
|
+
return h * 60 + m
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_window(s: str) -> Window:
|
|
76
|
+
spec = s.strip()
|
|
77
|
+
days: frozenset[int] = frozenset()
|
|
78
|
+
parts = spec.split(None, 1)
|
|
79
|
+
if len(parts) == 2 and _WINDOW_RE.match(parts[1]):
|
|
80
|
+
days = _parse_weekday_spec(parts[0])
|
|
81
|
+
time_part = parts[1]
|
|
82
|
+
else:
|
|
83
|
+
time_part = spec
|
|
84
|
+
match = _WINDOW_RE.match(time_part)
|
|
85
|
+
if not match:
|
|
86
|
+
raise ValueError(f"invalid window {s!r}: expected [WEEKDAYS ]HH:MM-HH:MM")
|
|
87
|
+
sh, sm, eh, em = (int(g) for g in match.groups())
|
|
88
|
+
start = _to_minutes(sh, sm, allow_24=False, where=f"window {s!r} start")
|
|
89
|
+
end = _to_minutes(eh, em, allow_24=True, where=f"window {s!r} end")
|
|
90
|
+
if start == end:
|
|
91
|
+
raise ValueError(f"invalid window {s!r}: start equals end (zero-length)")
|
|
92
|
+
return Window(start_min=start, end_min=end, label=s, days=days)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _minute_of_day(now_local: datetime) -> int:
|
|
96
|
+
return now_local.hour * 60 + now_local.minute
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def should_run(
|
|
100
|
+
now_local: datetime,
|
|
101
|
+
*,
|
|
102
|
+
run_windows: Sequence[Window],
|
|
103
|
+
pause_windows: Sequence[Window],
|
|
104
|
+
) -> bool:
|
|
105
|
+
minute = _minute_of_day(now_local)
|
|
106
|
+
weekday = now_local.weekday()
|
|
107
|
+
in_run = (not run_windows) or any(w.contains(weekday, minute) for w in run_windows)
|
|
108
|
+
in_pause = any(w.contains(weekday, minute) for w in pause_windows)
|
|
109
|
+
return in_run and not in_pause
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def next_resume_at(
|
|
113
|
+
now_local: datetime,
|
|
114
|
+
*,
|
|
115
|
+
run_windows: Sequence[Window],
|
|
116
|
+
pause_windows: Sequence[Window],
|
|
117
|
+
) -> datetime | None:
|
|
118
|
+
"""First minute-boundary within the next 8 days where should_run flips True.
|
|
119
|
+
Called only while currently paused; returns None if no window opens in 8 days."""
|
|
120
|
+
base = now_local.replace(second=0, microsecond=0)
|
|
121
|
+
for k in range(1, _SEARCH_HORIZON_MIN + 1):
|
|
122
|
+
cand = base + timedelta(minutes=k)
|
|
123
|
+
if should_run(cand, run_windows=run_windows, pause_windows=pause_windows):
|
|
124
|
+
return cand
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _active_window_label(
|
|
129
|
+
now_local: datetime,
|
|
130
|
+
*,
|
|
131
|
+
run_windows: Sequence[Window],
|
|
132
|
+
pause_windows: Sequence[Window],
|
|
133
|
+
) -> str:
|
|
134
|
+
minute = _minute_of_day(now_local)
|
|
135
|
+
weekday = now_local.weekday()
|
|
136
|
+
for w in pause_windows:
|
|
137
|
+
if w.contains(weekday, minute):
|
|
138
|
+
return w.label
|
|
139
|
+
return "outside run_windows"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class PauseDecision:
|
|
144
|
+
paused: bool
|
|
145
|
+
resume_at: datetime | None
|
|
146
|
+
active_window: str | None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def evaluate(
|
|
150
|
+
*,
|
|
151
|
+
run_windows: Sequence[Window],
|
|
152
|
+
pause_windows: Sequence[Window],
|
|
153
|
+
now_local: datetime,
|
|
154
|
+
) -> PauseDecision:
|
|
155
|
+
if should_run(now_local, run_windows=run_windows, pause_windows=pause_windows):
|
|
156
|
+
return PauseDecision(paused=False, resume_at=None, active_window=None)
|
|
157
|
+
return PauseDecision(
|
|
158
|
+
paused=True,
|
|
159
|
+
resume_at=next_resume_at(now_local, run_windows=run_windows, pause_windows=pause_windows),
|
|
160
|
+
active_window=_active_window_label(
|
|
161
|
+
now_local, run_windows=run_windows, pause_windows=pause_windows
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def now_in_zone(tz_name: str | None) -> datetime:
|
|
167
|
+
"""Timezone-aware now. tz_name None → host local time."""
|
|
168
|
+
if tz_name is None:
|
|
169
|
+
return datetime.now().astimezone()
|
|
170
|
+
return datetime.now(ZoneInfo(tz_name))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def valid_timezone(tz_name: str) -> bool:
|
|
174
|
+
try:
|
|
175
|
+
ZoneInfo(tz_name)
|
|
176
|
+
except (ZoneInfoNotFoundError, ValueError):
|
|
177
|
+
return False
|
|
178
|
+
return True
|
|
@@ -226,6 +226,8 @@ See `docs/plugins.md` for the `DirtyHandler` protocol and override recipe.
|
|
|
226
226
|
- `round_substrate_after`
|
|
227
227
|
- `round_substrate_before`
|
|
228
228
|
- `round_timeout_kill`
|
|
229
|
+
- `schedule_paused`
|
|
230
|
+
- `schedule_resumed`
|
|
229
231
|
- `serve_startup_hook_failed`
|
|
230
232
|
- `service_upgrade_rollback_failed`
|
|
231
233
|
- `service_upgrade_rolled_back`
|