cli-agent-runner 0.2.5__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.5 → cli_agent_runner-0.2.7}/.vulture-whitelist.py +23 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/CHANGELOG.md +27 -1
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/PKG-INFO +3 -3
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/README.md +1 -1
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_docgen.py +2 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_emit.py +61 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/api.py +4 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/api_types.py +1 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/serve_cmd.py +93 -1
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/config.py +46 -3
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/defenses.py +11 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/events.py +3 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/monitor.py +52 -1
- cli_agent_runner-0.2.7/agent_runner/round_log.py +160 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/runner.py +11 -2
- cli_agent_runner-0.2.7/agent_runner/schedule.py +178 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/architecture.md +6 -2
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/configuration.md +139 -1
- cli_agent_runner-0.2.7/docs/migrations/0.2.6.md +127 -0
- cli_agent_runner-0.2.7/docs/migrations/0.2.7.md +121 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/recipes/aider.md +6 -5
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/runbook.md +87 -7
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/_test_helpers.py +2 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_architecture.py +4 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config.py +52 -3
- cli_agent_runner-0.2.7/tests/unit/test_config_schedule.py +65 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_defenses.py +2 -2
- cli_agent_runner-0.2.7/tests/unit/test_emit_schedule.py +32 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_events.py +28 -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_round_log_helpers.py +269 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_runner.py +67 -0
- cli_agent_runner-0.2.7/tests/unit/test_schedule.py +183 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_serve_round_log.py +99 -24
- cli_agent_runner-0.2.7/tests/unit/test_serve_schedule_gate.py +143 -0
- cli_agent_runner-0.2.5/agent_runner/round_log.py +0 -95
- cli_agent_runner-0.2.5/tests/unit/test_round_log_helpers.py +0 -143
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/.gitignore +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/BACKLOG.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/LICENSE +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/README.zh.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/SECURITY.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/_throttle.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/agent_runtime.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/claude_rate_limit.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/codewhale.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/gemini.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/kimi.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/builtin_plugins/pi.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/events_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/peek_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/service_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/cli/upgrade_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/http_progress.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/metrics.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/remote_relay.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/agent_runner/vcs_state.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/build.sh +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/README.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/commands.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/events.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.16.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.17.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.19.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.20.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.21.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.22.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.23.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.24.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.25.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.26.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.27.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.28.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.29.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.30.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.31.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.32.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.33.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.34.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.35.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.36.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.37.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.38.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.39.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.40.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.1.42.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.2.0.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.2.1.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.2.2.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.2.3.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.2.4.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/migrations/0.2.5.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/plugins.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/pyproject.toml +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.2.5 → 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.5 → 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.5 → 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.5 → cli_agent_runner-0.2.7}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_observation.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_claude_error_detector.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_events_cmd_tail.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_gemini_plugin.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_kimi_plugin.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_metrics.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_pi_plugin.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_runner_throttle.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.5 → cli_agent_runner-0.2.7}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.2.5 → 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
|
|
@@ -228,6 +235,22 @@ path
|
|
|
228
235
|
# agent_runner.monitor.LocalSource
|
|
229
236
|
log_dir
|
|
230
237
|
|
|
238
|
+
# agent_runner.round_log.PruneOutcome
|
|
239
|
+
deleted
|
|
240
|
+
deferred
|
|
241
|
+
existing
|
|
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
|
+
|
|
231
254
|
# agent_runner.startup_check.CheckResult
|
|
232
255
|
name
|
|
233
256
|
ok
|
|
@@ -5,6 +5,31 @@ 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
|
+
|
|
20
|
+
## [0.2.6] - 2026-07-28
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- **Round-log pruning is now opt-in.** `runtime.round_log_retention` defaults to `0` (never prune) and `0` is now a legal value — previously both `0` and `-1` were rejected, so there was no off switch. Retaining too much is defended (`disk_warning` at 90%, `disk_critical` at 95% auto-stops the service); deleting too much was not defended at all. Both round-log families are affected, including the serve-level `round-<N>.log` family that had been capped at 100 for many releases. Set `round_log_retention = 100` for the previous behavior.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- A prune that would delete more files than it keeps is now refused wholesale — nothing is deleted — so opting in on an existing backlog, or lowering the value later, can no longer wipe history in one pass. 0.2.4 and 0.2.5 did exactly that on their first post-upgrade round (one deployment: 12,193 of 12,293 transcripts, ~6.4 GB).
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- `round_logs_prune_deferred` event — emitted on every prune attempt the bulk guard defers, carrying `directory`, `existing`, `keep`, `would_delete` and a hint naming `runtime.round_log_retention`. Resolve by raising retention above the backlog or deleting the files yourself; the guard never blocks a round. Retention `0` emits nothing.
|
|
30
|
+
|
|
31
|
+
See `docs/migrations/0.2.6.md`.
|
|
32
|
+
|
|
8
33
|
## [0.2.5] - 2026-07-27
|
|
9
34
|
|
|
10
35
|
### Added
|
|
@@ -950,7 +975,8 @@ Initial public release on PyPI as `cli-agent-runner`.
|
|
|
950
975
|
- Tag-triggered release publishing to PyPI via Trusted Publishing OIDC,
|
|
951
976
|
gated by a manual approval on the `pypi` GitHub environment.
|
|
952
977
|
|
|
953
|
-
[Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.
|
|
978
|
+
[Unreleased]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.6...HEAD
|
|
979
|
+
[0.2.6]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.5...v0.2.6
|
|
954
980
|
[0.2.5]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.4...v0.2.5
|
|
955
981
|
[0.2.4]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.3...v0.2.4
|
|
956
982
|
[0.2.3]: https://github.com/wan9yu/cli-agent-runner/compare/v0.2.2...v0.2.3
|
|
@@ -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
|
|
@@ -93,7 +93,7 @@ Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
|
93
93
|
|
|
94
94
|
## Defenses (built in)
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
13 named defenses, structured as data — see `agent-runner peek --select defenses`.
|
|
97
97
|
Each carries the historical incident it codifies and the invariant test that
|
|
98
98
|
guards it. Highlights:
|
|
99
99
|
|
|
@@ -57,7 +57,7 @@ Verb reference: [`docs/commands.md`](docs/commands.md).
|
|
|
57
57
|
|
|
58
58
|
## Defenses (built in)
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
13 named defenses, structured as data — see `agent-runner peek --select defenses`.
|
|
61
61
|
Each carries the historical incident it codifies and the invariant test that
|
|
62
62
|
guards it. Highlights:
|
|
63
63
|
|
|
@@ -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
|
|
|
@@ -22,9 +22,12 @@ __all__ = [
|
|
|
22
22
|
"emit_rate_limit_stop",
|
|
23
23
|
"emit_round_grace_extended",
|
|
24
24
|
"emit_round_grace_kill",
|
|
25
|
+
"emit_round_logs_prune_deferred",
|
|
25
26
|
"emit_round_progress",
|
|
26
27
|
"emit_round_substrate_after",
|
|
27
28
|
"emit_round_substrate_before",
|
|
29
|
+
"emit_schedule_paused",
|
|
30
|
+
"emit_schedule_resumed",
|
|
28
31
|
"emit_stop_file_detected",
|
|
29
32
|
"emit_transient_error_backoff_capped",
|
|
30
33
|
"emit_transient_error_detected",
|
|
@@ -84,6 +87,28 @@ def emit_stop_file_detected(
|
|
|
84
87
|
)
|
|
85
88
|
|
|
86
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
|
+
|
|
87
112
|
def emit_round_substrate_before(
|
|
88
113
|
log_dir: Path, *, round_num: int, git_head: str | None, paths_hash: str | None
|
|
89
114
|
) -> None:
|
|
@@ -346,6 +371,42 @@ def emit_round_grace_extended(
|
|
|
346
371
|
)
|
|
347
372
|
|
|
348
373
|
|
|
374
|
+
def emit_round_logs_prune_deferred(
|
|
375
|
+
log_dir: Path,
|
|
376
|
+
*,
|
|
377
|
+
directory: str,
|
|
378
|
+
existing: int,
|
|
379
|
+
keep: int,
|
|
380
|
+
would_delete: int,
|
|
381
|
+
) -> None:
|
|
382
|
+
"""Emit when a round-log prune was deferred because it would be *bulk*
|
|
383
|
+
(it would delete more files than it keeps), so nothing was deleted.
|
|
384
|
+
|
|
385
|
+
``directory`` is the directory holding the family — ``{log_dir}/rounds``
|
|
386
|
+
for agent transcripts, ``{log_dir}`` for the serve-level ``round-<N>.log``
|
|
387
|
+
files. Re-emitted on every prune attempt while the condition holds: the
|
|
388
|
+
deferral is permanent until an operator acts, and a one-shot event would
|
|
389
|
+
be missed by anyone who started watching later.
|
|
390
|
+
|
|
391
|
+
The hint is composed here rather than at the call sites so both families
|
|
392
|
+
name the same knob with the same wording.
|
|
393
|
+
"""
|
|
394
|
+
from agent_runner.events import ROUND_LOGS_PRUNE_DEFERRED, emit
|
|
395
|
+
|
|
396
|
+
emit(
|
|
397
|
+
log_dir,
|
|
398
|
+
ROUND_LOGS_PRUNE_DEFERRED,
|
|
399
|
+
directory=directory,
|
|
400
|
+
existing=existing,
|
|
401
|
+
keep=keep,
|
|
402
|
+
would_delete=would_delete,
|
|
403
|
+
hint=(
|
|
404
|
+
f"nothing deleted; raise runtime.round_log_retention to >= {existing} "
|
|
405
|
+
f"to keep this backlog, or delete files in {directory} yourself"
|
|
406
|
+
),
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
349
410
|
def emit_anomaly_repetitive_tool(
|
|
350
411
|
log_dir: Path,
|
|
351
412
|
*,
|
|
@@ -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
|
|
|
@@ -742,9 +743,12 @@ from agent_runner._emit import ( # noqa: E402,F401 — intentional bottom re-ex
|
|
|
742
743
|
emit_rate_limit_stop,
|
|
743
744
|
emit_round_grace_extended,
|
|
744
745
|
emit_round_grace_kill,
|
|
746
|
+
emit_round_logs_prune_deferred,
|
|
745
747
|
emit_round_progress,
|
|
746
748
|
emit_round_substrate_after,
|
|
747
749
|
emit_round_substrate_before,
|
|
750
|
+
emit_schedule_paused,
|
|
751
|
+
emit_schedule_resumed,
|
|
748
752
|
emit_stop_file_detected,
|
|
749
753
|
emit_transient_error_backoff_capped,
|
|
750
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)
|
|
@@ -15,7 +15,9 @@ import signal
|
|
|
15
15
|
import subprocess # noqa: TID251
|
|
16
16
|
import sys
|
|
17
17
|
import time
|
|
18
|
+
from pathlib import Path
|
|
18
19
|
|
|
20
|
+
from agent_runner import schedule
|
|
19
21
|
from agent_runner._substrate import compute_git_head, compute_paths_hash
|
|
20
22
|
from agent_runner._throttle import _check_throttle_state
|
|
21
23
|
from agent_runner._throttle import reset_counters as _reset_counters
|
|
@@ -26,8 +28,11 @@ from agent_runner.api import (
|
|
|
26
28
|
emit_fresh_eyes_round_triggered,
|
|
27
29
|
emit_max_rounds_reached,
|
|
28
30
|
emit_rate_limit_stop,
|
|
31
|
+
emit_round_logs_prune_deferred,
|
|
29
32
|
emit_round_substrate_after,
|
|
30
33
|
emit_round_substrate_before,
|
|
34
|
+
emit_schedule_paused,
|
|
35
|
+
emit_schedule_resumed,
|
|
31
36
|
emit_stop_file_detected,
|
|
32
37
|
post_round_decision,
|
|
33
38
|
)
|
|
@@ -56,6 +61,86 @@ def _resolve_max_rounds(*, cli_value: int | None, config_value: int | None) -> i
|
|
|
56
61
|
return effective
|
|
57
62
|
|
|
58
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
|
+
|
|
126
|
+
def _prune_serve_round_logs(log_dir: Path, retention: int) -> None:
|
|
127
|
+
"""Prune the serve-level ``round-<N>.log`` family; report a deferred prune.
|
|
128
|
+
|
|
129
|
+
A prune that would delete more files than it keeps is deferred wholesale
|
|
130
|
+
(nothing is deleted) and surfaced as ``round_logs_prune_deferred`` — the
|
|
131
|
+
same contract ``rounds/`` gets at round start, since one knob governs both.
|
|
132
|
+
"""
|
|
133
|
+
outcome = prune_old_round_logs(log_dir, retention)
|
|
134
|
+
if outcome.deferred:
|
|
135
|
+
emit_round_logs_prune_deferred(
|
|
136
|
+
log_dir,
|
|
137
|
+
directory=str(log_dir),
|
|
138
|
+
existing=outcome.existing,
|
|
139
|
+
keep=retention,
|
|
140
|
+
would_delete=outcome.deferred,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
59
144
|
def _is_fresh_eyes_round(*, round_num: int, every_n: int | None) -> bool:
|
|
60
145
|
"""True if this round should signal fresh-eyes mode to the agent subprocess.
|
|
61
146
|
|
|
@@ -81,6 +166,11 @@ def add_parser(sub, parent) -> None:
|
|
|
81
166
|
p = sub.add_parser("serve", parents=[parent], help="Long-running supervisor loop")
|
|
82
167
|
p.add_argument("--once", action="store_true", help="Run a single round then exit (debug)")
|
|
83
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
|
+
)
|
|
84
174
|
p.set_defaults(func=cmd)
|
|
85
175
|
|
|
86
176
|
|
|
@@ -106,7 +196,7 @@ def cmd(args) -> int:
|
|
|
106
196
|
|
|
107
197
|
# Pre-loop cleanup: remove stale sentinel, prune old round logs.
|
|
108
198
|
(log_dir / ".agent-done").unlink(missing_ok=True)
|
|
109
|
-
|
|
199
|
+
_prune_serve_round_logs(log_dir, cfg.runtime.round_log_retention)
|
|
110
200
|
|
|
111
201
|
round_env = {**os.environ, "AGENT_RUNNER_LOG_DIR": str(log_dir)}
|
|
112
202
|
|
|
@@ -157,6 +247,8 @@ def cmd(args) -> int:
|
|
|
157
247
|
max_rounds=effective_max_rounds,
|
|
158
248
|
)
|
|
159
249
|
break
|
|
250
|
+
if not args.ignore_schedule and _maybe_pause_for_schedule(cfg, log_dir, stop):
|
|
251
|
+
continue
|
|
160
252
|
round_num = next_round_num(log_dir)
|
|
161
253
|
git_head_before = compute_git_head(work_dir)
|
|
162
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"})
|
|
@@ -39,7 +41,7 @@ class RuntimeConfig:
|
|
|
39
41
|
round_timeout_s: int = 1800
|
|
40
42
|
restart_delay_s: int = 3
|
|
41
43
|
disable_pre_round_hooks: bool = False
|
|
42
|
-
round_log_retention: int =
|
|
44
|
+
round_log_retention: int = 0 # 0 = never prune (pruning is opt-in)
|
|
43
45
|
narrative_file: Path | None = None
|
|
44
46
|
transient_error_action: Literal["back_off", "skip", "stop"] = "back_off"
|
|
45
47
|
max_rounds: int | None = None # None = unbounded
|
|
@@ -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}")
|
|
@@ -439,8 +479,8 @@ def load_config(toml_path: Path) -> Config:
|
|
|
439
479
|
runtime_d.get("disable_pre_round_hooks", False),
|
|
440
480
|
field="runtime.disable_pre_round_hooks",
|
|
441
481
|
),
|
|
442
|
-
round_log_retention=
|
|
443
|
-
runtime_d.get("round_log_retention",
|
|
482
|
+
round_log_retention=_require_non_negative_int(
|
|
483
|
+
runtime_d.get("round_log_retention", 0), field="runtime.round_log_retention"
|
|
444
484
|
),
|
|
445
485
|
narrative_file=_expand_and_resolve(str(runtime_d["narrative_file"]), project_name, work_dir)
|
|
446
486
|
if "narrative_file" in runtime_d
|
|
@@ -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
|
|
@@ -102,6 +102,17 @@ def catalog(cfg: Config) -> list[Defense]:
|
|
|
102
102
|
guarded_by=Path("tests/unit/test_serve_crash_loop.py"),
|
|
103
103
|
current_state="active",
|
|
104
104
|
),
|
|
105
|
+
Defense(
|
|
106
|
+
name="bulk_round_log_prune_guard",
|
|
107
|
+
value="a prune deleting more files than it keeps deletes nothing",
|
|
108
|
+
codifies=(
|
|
109
|
+
"0.2.4 — rounds/ pruning shipped against backlogs it never built; "
|
|
110
|
+
"one deployment's first post-upgrade round would have deleted "
|
|
111
|
+
"12,193 of 12,293 transcripts silently"
|
|
112
|
+
),
|
|
113
|
+
guarded_by=Path("tests/unit/test_round_log_helpers.py"),
|
|
114
|
+
current_state="active",
|
|
115
|
+
),
|
|
105
116
|
Defense(
|
|
106
117
|
name="flock_concurrency",
|
|
107
118
|
value="agent-runner.lock",
|
|
@@ -55,11 +55,14 @@ PROMPT_OVERWRITTEN = "prompt_overwritten"
|
|
|
55
55
|
ROUND_END = "round_end"
|
|
56
56
|
ROUND_GRACE_EXTENDED = "round_grace_extended"
|
|
57
57
|
ROUND_GRACE_KILL = "round_grace_kill"
|
|
58
|
+
ROUND_LOGS_PRUNE_DEFERRED = "round_logs_prune_deferred"
|
|
58
59
|
ROUND_PROGRESS = "round_progress"
|
|
59
60
|
ROUND_START = "round_start"
|
|
60
61
|
ROUND_SUBSTRATE_AFTER = "round_substrate_after"
|
|
61
62
|
ROUND_SUBSTRATE_BEFORE = "round_substrate_before"
|
|
62
63
|
ROUND_TIMEOUT_KILL = "round_timeout_kill"
|
|
64
|
+
SCHEDULE_PAUSED = "schedule_paused"
|
|
65
|
+
SCHEDULE_RESUMED = "schedule_resumed"
|
|
63
66
|
SELF_TERMINATED = "agent_self_terminated"
|
|
64
67
|
SERVE_STARTUP_HOOK_FAILED = "serve_startup_hook_failed"
|
|
65
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
|
# ---------------------------------------------------------------------------
|