cli-agent-runner 0.2.9__tar.gz → 0.2.10__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.9 → cli_agent_runner-0.2.10}/.vulture-whitelist.py +2 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/CHANGELOG.md +12 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/PKG-INFO +1 -1
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/_emit.py +7 -1
- cli_agent_runner-0.2.10/agent_runner/_throttle.py +220 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/_version.py +2 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/agent_runtime.py +14 -12
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/api.py +7 -7
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/api_types.py +2 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/builtin_plugins/claude_rate_limit.py +6 -4
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/builtin_plugins/codewhale.py +6 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/builtin_plugins/gemini.py +5 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/builtin_plugins/kimi.py +5 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/builtin_plugins/pi.py +3 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/cli/events_cmd.py +3 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/cli/peek_cmd.py +2 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/cli/serve_cmd.py +132 -40
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/cli/service_cmd.py +3 -4
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/cli/upgrade_cmd.py +10 -10
- cli_agent_runner-0.2.10/agent_runner/clock.py +79 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/events.py +5 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/http_progress.py +1 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/metrics.py +2 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/monitor.py +3 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/phase_select.py +30 -12
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/remote_relay.py +3 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/runner.py +11 -9
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/schedule.py +5 -4
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/vcs_state.py +4 -3
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/configuration.md +5 -1
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/migrations/0.2.md +20 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/runbook.md +15 -1
- cli_agent_runner-0.2.10/tests/_clock.py +43 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_architecture.py +3 -1
- cli_agent_runner-0.2.10/tests/invariants/test_no_raw_time.py +136 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_phase_select_stateless.py +11 -8
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_api_observation.py +1 -1
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_claude_error_detector.py +6 -6
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_events.py +2 -4
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_events_cmd_tail.py +2 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_gemini_plugin.py +2 -2
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_kimi_plugin.py +1 -1
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_metrics.py +2 -7
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_phase_select.py +47 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_pi_plugin.py +1 -1
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_runner_throttle.py +115 -27
- cli_agent_runner-0.2.10/tests/unit/test_serve_phase_schedule.py +454 -0
- cli_agent_runner-0.2.9/agent_runner/_throttle.py +0 -142
- cli_agent_runner-0.2.9/tests/unit/test_serve_phase_schedule.py +0 -193
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/.codecov.yml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/.github/workflows/ci.yml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/.github/workflows/release.yml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/.gitignore +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/BACKLOG.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/CODE_OF_CONDUCT.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/CONTRIBUTING.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/LICENSE +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/README.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/README.zh.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/SECURITY.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/_docgen.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/_redact.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/_registry.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/_substrate.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/builtin_plugins/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/builtin_plugins/_constants.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/builtin_plugins/default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/cli/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/cli/__main__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/cli/common.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/cli/init_cmd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/cli/install_cmd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/cli/migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/cli/monitor_cmd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/cli/round_cmd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/config.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/context_store.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/defenses.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/hooks.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/lifecycle.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/migrations.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/presets/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/presets/aider.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/presets/claude.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/presets/codewhale.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/presets/gemini.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/presets/kimi.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/presets/pi.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/prompt_loader.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/round_log.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/round_view.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/scaffold.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/service_unit.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/startup_check.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/build.sh +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/README.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/architecture.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/commands.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/events.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/long-running-agents.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/plugins.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/quickstart.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/recipes/aider.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/recipes/codewhale.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/recipes/kimi.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/recipes/pi.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/docs/thesis.md +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/pyproject.toml +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/_test_helpers.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/conftest.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/contract/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/contract/test_public_api_surface.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/e2e/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/e2e/conftest.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/e2e/test_e2e_graceful_stop.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/e2e/test_e2e_install_systemd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/e2e/test_e2e_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/e2e/test_e2e_round_lifecycle.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/fixtures/cli-real-output/claude-2.1.143-assistant-tool-use.jsonl +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/fixtures/cli-real-output/claude-2.1.143-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/fixtures/cli-real-output/gemini-0.42.0-result-event.jsonl +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/generate_vulture_whitelist.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_bounded_run.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_context_enricher_namespacing.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_dirty_handler_seam.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_fresh_eyes_signal.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_grace_kill_emission.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_install_dry_run.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_monitor_seeded.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_plugin_detector_loaded.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_plugin_owned_paths.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_plugin_real_flow.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_prompt_delivery_stdin.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_run_one_round_with_fake_agent.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_scaffold_presets.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_serve_loop.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_substrate_fingerprint.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/integration/test_transient_error_backoff.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/_docs.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/_event_scan.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_atomic_write_enforced.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_authored_facts_declared.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_auto_stop_policy_ssot.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_classification_ssot.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_config_error_consistency.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_config_value_set_ssot.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_doc_builtin_plugins.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_doc_claims_match_ssot.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_doc_cli_claims.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_doc_operator_surface.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_doc_test_pointers_exist.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_doc_transient_error_claims.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_doc_usage_event_fields.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_docs_generated.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_docs_index_complete.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_entry_points_resolve.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_event_kind_registry.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_event_kinds_ssot.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_events_doc_contract.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_gen_blocks_name_their_source.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_hook_contract_docs.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_layer_2_loop_size.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_module_boundaries.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_module_sizes.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_no_ai_signatures.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_no_pytest_skip_on_parse_fail.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_peek_schema_version.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_plugins_config_stable.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_readme_zh_is_pointer.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_repo_constants_patched_in_tests.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_round_result_stable.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_set_diff_for_auto_tool_classification.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_stash_uses_sha_not_index.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_upstream_schema_canary.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/invariants/test_vulture_whitelist_generated.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/literate/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/literate/parser.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/literate/test_parser.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/literate/test_quickstart.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/__init__.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_agent_runtime.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_agent_runtime_grace.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_agent_runtime_progress.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_agent_runtime_signal_name.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_api_assemble_prompt.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_api_events_stream.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_api_install.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_api_read_round_num.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_api_resolve_phase.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_api_service.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_api_types.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_auto_stop_gating.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_catalogs.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_cli.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_cli_common.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_cli_init_install.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_cli_monitor_http.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_cli_service_peek_monitor.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_cli_upgrade.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_codewhale_plugin.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_config.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_config_fresh_eyes.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_config_max_rounds.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_config_prompt_delivery.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_config_schedule.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_config_stop_file.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_config_substrate_fingerprint_paths.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_config_transient_error_action.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_context_store.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_default_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_defenses.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_detector_protocol.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_dirty_handlers.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_docgen.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_emit_config_migrated.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_emit_schedule.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_emit_schedule_phase_skipped.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_events_cmd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_example_dirty_handler.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_example_plugin.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_exit_cause.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_fresh_eyes_trigger.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_hook_failure_isolation.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_hooks.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_http_progress.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_init_entry_points.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_lifecycle.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_migrate_cmd.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_migrations.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_monitor_assembly.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_monitor_detect_anomaly_repetitive.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_monitor_detect_rate_limit.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_monitor_detect_supervisor_stale.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_monitor_detectors.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_monitor_remote.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_monitor_schedule_pause.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_network_blip_signal_guard.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_peek_argparse.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_peek_schedule.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_peek_select.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_phase_agent_launch.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_phase_profiles.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_plugin_constants.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_presets.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_prompt_loader.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_redact.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_round_log_helpers.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_round_view.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_runner.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_scaffold.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_schedule.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_serve_cmd_bounded.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_serve_config_broken.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_serve_crash_loop.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_serve_round_log.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_serve_schedule_gate.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_serve_sentinel.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_serve_startup_hooks.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_service_unit.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_startup_check.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_substrate.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_throttle_plugin_classification.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_upgrade_migrate.py +0 -0
- {cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/tests/unit/test_vcs_state.py +0 -0
|
@@ -57,6 +57,7 @@ throttled_until_epoch
|
|
|
57
57
|
limit_type
|
|
58
58
|
agent
|
|
59
59
|
since_round
|
|
60
|
+
phase
|
|
60
61
|
|
|
61
62
|
# agent_runner.api_types.RoundResult
|
|
62
63
|
round_num
|
|
@@ -106,6 +107,7 @@ reset_at_epoch
|
|
|
106
107
|
classification
|
|
107
108
|
agent
|
|
108
109
|
since_round
|
|
110
|
+
phase
|
|
109
111
|
|
|
110
112
|
# agent_runner.config.AgentConfig
|
|
111
113
|
command
|
|
@@ -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.10] - 2026-08-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Throttle-aware skip: under `[phases] phase_policy = "skip"`, a phase whose provider is currently rate-limited/erroring is stepped over in the rotation — a healthy sibling runs instead of the whole loop sleeping on the throttle. No new config; the trigger is the existing `skip` policy.
|
|
12
|
+
- `transient_error_detected` now carries the failing `phase`; `peek --json` `rate_limit` and the HTTP progress state surface which phase is throttled.
|
|
13
|
+
- `transient_error_recovered` is emitted when a skip-around throttle clears (events-derived, restart-safe, no double-emit with the back-off path).
|
|
14
|
+
|
|
15
|
+
### Notes
|
|
16
|
+
- Only `skip` routes around a throttle; `wait`, `--ignore-schedule`, and non-`[phases]` configs keep the 0.2.9 global back-off. When every phase is throttled or window-closed, the loop waits until the earlier of a window opening or the throttle resetting.
|
|
17
|
+
|
|
18
|
+
See `docs/migrations/0.2.md`.
|
|
19
|
+
|
|
8
20
|
## [0.2.9] - 2026-08-28
|
|
9
21
|
|
|
10
22
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cli-agent-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.10
|
|
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
|
|
@@ -188,8 +188,13 @@ def emit_transient_error_detected(
|
|
|
188
188
|
reset_at_epoch: int,
|
|
189
189
|
round_num: int,
|
|
190
190
|
raw: str,
|
|
191
|
+
phase: str = "",
|
|
191
192
|
) -> None:
|
|
192
|
-
"""Emit detection of a transient agent error (rate limit / 5xx / timeout).
|
|
193
|
+
"""Emit detection of a transient agent error (rate limit / 5xx / timeout).
|
|
194
|
+
|
|
195
|
+
``phase`` is the rotation phase the failing round ran under ("" when the
|
|
196
|
+
config has no ``[phases]``). It lets a ``phase_policy = "skip"`` serve loop
|
|
197
|
+
step over *this* phase while a healthy sibling keeps running."""
|
|
193
198
|
from agent_runner._redact import redact_secrets
|
|
194
199
|
from agent_runner.events import TRANSIENT_ERROR_DETECTED, emit
|
|
195
200
|
|
|
@@ -202,6 +207,7 @@ def emit_transient_error_detected(
|
|
|
202
207
|
reset_at_epoch=reset_at_epoch,
|
|
203
208
|
round_num=round_num,
|
|
204
209
|
raw=raw,
|
|
210
|
+
phase=phase,
|
|
205
211
|
)
|
|
206
212
|
|
|
207
213
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Throttle state helpers — read events.jsonl tail for transient error state.
|
|
2
|
+
|
|
3
|
+
Internal module. Callers: runner.py (serve loop back-off), api.py (peek).
|
|
4
|
+
Separated from runner.py to satisfy the ouroboros defense: runner.py writes
|
|
5
|
+
events.jsonl but must never read it back (§3 module boundary invariant).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from datetime import UTC
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from agent_runner.api_types import TransientErrorState
|
|
16
|
+
from agent_runner.clock import SYSTEM_CLOCK, Clock
|
|
17
|
+
from agent_runner.events import (
|
|
18
|
+
TRANSIENT_ERROR_DETECTED,
|
|
19
|
+
TRANSIENT_ERROR_RECOVERED,
|
|
20
|
+
parse_iso_ms,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# _scan_events_for_transient sentinel: file held no transient event at all
|
|
24
|
+
# (distinct from "latest transient was a recovered" → None).
|
|
25
|
+
_NO_TRANSIENT = object()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _scan_events_for_transient(path: Path):
|
|
29
|
+
"""The file's LAST transient event: the detected dict, ``None`` (latest was a
|
|
30
|
+
recovered), or ``_NO_TRANSIENT`` (no transient event in the file).
|
|
31
|
+
|
|
32
|
+
Forward single-pass, O(1) memory — it keeps only the latest transient event,
|
|
33
|
+
never a 100-line tail. That matters for throttle-aware skip: the loop keeps
|
|
34
|
+
emitting rounds on healthy phases during a throttle, so the ``detected`` event
|
|
35
|
+
can be thousands of lines back; a bounded tail would scroll it out and make the
|
|
36
|
+
supervisor forget the throttle mid-window."""
|
|
37
|
+
latest: Any = _NO_TRANSIENT
|
|
38
|
+
with path.open() as f:
|
|
39
|
+
for line in f:
|
|
40
|
+
line = line.strip()
|
|
41
|
+
if not line:
|
|
42
|
+
continue
|
|
43
|
+
try:
|
|
44
|
+
ev = json.loads(line)
|
|
45
|
+
except json.JSONDecodeError:
|
|
46
|
+
continue
|
|
47
|
+
kind = ev.get("event")
|
|
48
|
+
if kind == TRANSIENT_ERROR_DETECTED:
|
|
49
|
+
latest = ev
|
|
50
|
+
elif kind == TRANSIENT_ERROR_RECOVERED:
|
|
51
|
+
latest = None
|
|
52
|
+
return latest
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _latest_unrecovered_detected(log_dir: Path) -> dict[str, Any] | None:
|
|
56
|
+
"""The most recent ``transient_error_detected`` with no
|
|
57
|
+
``transient_error_recovered`` after it, or None. Shared by
|
|
58
|
+
:func:`_check_throttle_state` (still-throttled?) and :func:`pending_recovered`
|
|
59
|
+
(cleared-without-a-breadcrumb?).
|
|
60
|
+
|
|
61
|
+
Scans the newest monthly ``events-*.jsonl`` and, only if it holds no transient
|
|
62
|
+
event yet, the previous month's — so a throttle that spans a month boundary
|
|
63
|
+
(detected in the old file, cleared in the new) is not orphaned."""
|
|
64
|
+
candidates = sorted(log_dir.glob("events-*.jsonl"))
|
|
65
|
+
for path in reversed(candidates[-2:]):
|
|
66
|
+
result = _scan_events_for_transient(path)
|
|
67
|
+
if result is not _NO_TRANSIENT:
|
|
68
|
+
return result # a detected dict, or None (latest transient was recovered)
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _check_throttle_state(
|
|
73
|
+
log_dir: Path, *, clock: Clock = SYSTEM_CLOCK
|
|
74
|
+
) -> TransientErrorState | None:
|
|
75
|
+
"""Scan events.jsonl tail for latest unmatched transient error.
|
|
76
|
+
|
|
77
|
+
Reads `transient_error_detected` / `transient_error_recovered` event names.
|
|
78
|
+
Returns TransientErrorState if currently throttled (reset still in future,
|
|
79
|
+
no matching recovered after). Restart-safe.
|
|
80
|
+
|
|
81
|
+
``clock`` supplies the wall clock the reset is compared against — inject a
|
|
82
|
+
``FakeClock`` in tests to pin an exact instant. Only the reset comparison
|
|
83
|
+
reads it; the event scan is pure.
|
|
84
|
+
"""
|
|
85
|
+
now = clock.epoch()
|
|
86
|
+
latest_detected = _latest_unrecovered_detected(log_dir)
|
|
87
|
+
if latest_detected is None:
|
|
88
|
+
return None
|
|
89
|
+
reset_at = int(latest_detected.get("reset_at_epoch", 0))
|
|
90
|
+
if reset_at <= now:
|
|
91
|
+
return None # Reset already passed without recovery emit; treat as recovered
|
|
92
|
+
|
|
93
|
+
classification = str(latest_detected.get("classification", "rate_limit_account"))
|
|
94
|
+
|
|
95
|
+
return TransientErrorState(
|
|
96
|
+
reset_at_epoch=reset_at,
|
|
97
|
+
classification=classification,
|
|
98
|
+
agent=str(latest_detected.get("agent", "unknown")),
|
|
99
|
+
since_round=int(latest_detected.get("round_num", 0)),
|
|
100
|
+
phase=str(latest_detected.get("phase", "")),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _throttled_for_s(ts: Any, *, clock: Clock = SYSTEM_CLOCK) -> int:
|
|
105
|
+
"""Seconds since a detected event's ``ts`` (best-effort; 0 if unparseable).
|
|
106
|
+
``clock`` supplies now; inject a ``FakeClock`` for exact-value tests."""
|
|
107
|
+
if not ts:
|
|
108
|
+
return 0
|
|
109
|
+
try:
|
|
110
|
+
detected = parse_iso_ms(str(ts)) # events.py owns the Z→+00:00 workaround
|
|
111
|
+
except (ValueError, TypeError):
|
|
112
|
+
return 0
|
|
113
|
+
if detected.tzinfo is None:
|
|
114
|
+
detected = detected.replace(tzinfo=UTC) # hand-injected naive ts → treat as UTC
|
|
115
|
+
return max(0, int(clock.epoch() - detected.timestamp()))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def pending_recovered(log_dir: Path, *, clock: Clock = SYSTEM_CLOCK) -> tuple[str, str, int] | None:
|
|
119
|
+
"""``(classification, agent, throttled_for_s)`` iff a transient throttle
|
|
120
|
+
cleared WITHOUT a recovered breadcrumb — the latest
|
|
121
|
+
``transient_error_detected`` has ``reset_at <= now`` and NO
|
|
122
|
+
``transient_error_recovered`` after it.
|
|
123
|
+
|
|
124
|
+
This is the throttle-aware-skip path: the loop rotated to a healthy phase
|
|
125
|
+
instead of calling ``_apply_back_off`` (which emits its own recovered), so
|
|
126
|
+
the multi-hour throttle would otherwise close with no breadcrumb. Being
|
|
127
|
+
events-derived, it is restart-safe AND never double-emits — once the caller
|
|
128
|
+
emits recovered, that event sits after the detected one and the predicate
|
|
129
|
+
goes quiet. Returns None while still throttled (reset in the future) or when
|
|
130
|
+
the back-off path already left a recovered."""
|
|
131
|
+
now = clock.epoch()
|
|
132
|
+
latest_detected = _latest_unrecovered_detected(log_dir)
|
|
133
|
+
if latest_detected is None:
|
|
134
|
+
return None
|
|
135
|
+
if int(latest_detected.get("reset_at_epoch", 0)) > now:
|
|
136
|
+
return None # still throttled — not a clear transition
|
|
137
|
+
return (
|
|
138
|
+
str(latest_detected.get("classification", "rate_limit_account")),
|
|
139
|
+
str(latest_detected.get("agent", "unknown")),
|
|
140
|
+
_throttled_for_s(latest_detected.get("ts"), clock=clock),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Module-level supervisor state — bucket → consecutive-failure count.
|
|
145
|
+
# Cleared by reset_counters() or by serve restart.
|
|
146
|
+
_consecutive_failures: dict[str, int] = {}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def compute_adjusted_reset_at(
|
|
150
|
+
*,
|
|
151
|
+
classification: str,
|
|
152
|
+
original_reset_at_epoch: int,
|
|
153
|
+
agent: str,
|
|
154
|
+
log_dir: Path,
|
|
155
|
+
clock: Clock = SYSTEM_CLOCK,
|
|
156
|
+
) -> tuple[int, int, bool]:
|
|
157
|
+
"""Apply exp backoff for estimated-class transient errors.
|
|
158
|
+
|
|
159
|
+
Returns (applied_reset_at_epoch, consecutive_count, capped_by_absolute_max).
|
|
160
|
+
|
|
161
|
+
For server-authoritative classification (``rate_limit_account``): returns
|
|
162
|
+
the original reset epoch verbatim, never increments the counter, and
|
|
163
|
+
never emits an adjustment event. Anthropic's resetsAt is authoritative.
|
|
164
|
+
|
|
165
|
+
For any classification absent from ``_BACK_OFF_DEFAULTS`` — i.e. one a
|
|
166
|
+
third-party plugin defined itself, which ``api_types.py`` types
|
|
167
|
+
``classification`` as ``str`` to permit — the same verbatim path applies:
|
|
168
|
+
core has no base duration for it, so the emitter's reset_at_epoch is the
|
|
169
|
+
only non-invented answer.
|
|
170
|
+
|
|
171
|
+
For estimated classifications (``rate_limit_model``, ``api_transient_5xx``,
|
|
172
|
+
``api_timeout``): increments the counter for this bucket, computes
|
|
173
|
+
duration = base × 2^min(n, _EXP_CAP), caps at _ABSOLUTE_CAP_S, emits
|
|
174
|
+
``transient_error_backoff_capped`` if multiplier > 1 or capped.
|
|
175
|
+
"""
|
|
176
|
+
from agent_runner._emit import emit_transient_error_backoff_capped
|
|
177
|
+
from agent_runner.builtin_plugins._constants import (
|
|
178
|
+
_ABSOLUTE_CAP_S,
|
|
179
|
+
_BACK_OFF_DEFAULTS,
|
|
180
|
+
_EXP_CAP,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if classification == "rate_limit_account" or classification not in _BACK_OFF_DEFAULTS:
|
|
184
|
+
# Server-authoritative, or a plugin's own classification (api_types.py types
|
|
185
|
+
# `classification` as str precisely so plugins can add their own): the emitter
|
|
186
|
+
# supplied reset_at_epoch, so respect it verbatim and never touch the counter.
|
|
187
|
+
return (original_reset_at_epoch, 0, False)
|
|
188
|
+
|
|
189
|
+
# Estimated class: apply exp backoff.
|
|
190
|
+
base = _BACK_OFF_DEFAULTS[classification]
|
|
191
|
+
n = _consecutive_failures.get(classification, 0)
|
|
192
|
+
multiplier = 2 ** min(n, _EXP_CAP)
|
|
193
|
+
extended_duration = base * multiplier
|
|
194
|
+
capped_by_absolute_max = extended_duration > _ABSOLUTE_CAP_S
|
|
195
|
+
applied_duration = min(extended_duration, _ABSOLUTE_CAP_S)
|
|
196
|
+
applied_reset_at = int(clock.epoch()) + applied_duration
|
|
197
|
+
|
|
198
|
+
new_count = n + 1
|
|
199
|
+
_consecutive_failures[classification] = new_count
|
|
200
|
+
|
|
201
|
+
# Emit observability event when supervisor adjusted the wait.
|
|
202
|
+
if multiplier > 1 or capped_by_absolute_max:
|
|
203
|
+
emit_transient_error_backoff_capped(
|
|
204
|
+
log_dir,
|
|
205
|
+
classification=classification,
|
|
206
|
+
agent=agent,
|
|
207
|
+
requested_sleep_s=int(base),
|
|
208
|
+
applied_sleep_s=applied_duration,
|
|
209
|
+
original_reset_at_epoch=original_reset_at_epoch,
|
|
210
|
+
applied_reset_at_epoch=applied_reset_at,
|
|
211
|
+
consecutive_count=new_count,
|
|
212
|
+
capped_by_absolute_max=capped_by_absolute_max,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return (applied_reset_at, new_count, capped_by_absolute_max)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def reset_counters() -> None:
|
|
219
|
+
"""Clear all bucket counters. Called by serve loop when no active throttle."""
|
|
220
|
+
_consecutive_failures.clear()
|
|
@@ -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.10'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 10)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -15,13 +15,14 @@ import shutil
|
|
|
15
15
|
import signal
|
|
16
16
|
import subprocess # noqa: TID251 — sanctioned subprocess caller
|
|
17
17
|
import threading
|
|
18
|
-
import time
|
|
19
18
|
from collections.abc import Callable
|
|
20
19
|
from dataclasses import dataclass, field
|
|
21
20
|
from pathlib import Path
|
|
22
21
|
|
|
23
22
|
import psutil
|
|
24
23
|
|
|
24
|
+
from agent_runner.clock import SYSTEM_CLOCK, Clock
|
|
25
|
+
|
|
25
26
|
REAP_GRACE_S = 5
|
|
26
27
|
|
|
27
28
|
|
|
@@ -68,15 +69,15 @@ def _build_argv(command: list[str], prompt_arg_template: list[str], prompt: str)
|
|
|
68
69
|
return list(command) + [a.replace("{prompt}", prompt) for a in prompt_arg_template]
|
|
69
70
|
|
|
70
71
|
|
|
71
|
-
def _kill_pgroup(proc: subprocess.Popen) -> None:
|
|
72
|
+
def _kill_pgroup(proc: subprocess.Popen, clock: Clock = SYSTEM_CLOCK) -> None:
|
|
72
73
|
pgid = proc.pid
|
|
73
74
|
try:
|
|
74
75
|
os.killpg(pgid, signal.SIGTERM)
|
|
75
76
|
except OSError:
|
|
76
77
|
pass
|
|
77
|
-
deadline =
|
|
78
|
-
while
|
|
79
|
-
|
|
78
|
+
deadline = clock.epoch() + REAP_GRACE_S
|
|
79
|
+
while clock.epoch() < deadline and proc.poll() is None:
|
|
80
|
+
clock.sleep(0.1)
|
|
80
81
|
try:
|
|
81
82
|
os.killpg(pgid, signal.SIGKILL)
|
|
82
83
|
except OSError:
|
|
@@ -173,6 +174,7 @@ def run(
|
|
|
173
174
|
progress_interval_s: int = 0,
|
|
174
175
|
on_grace_extended: Callable[[list[dict], list[dict]], None] | None = None,
|
|
175
176
|
grace_kill_ignore_patterns: list[re.Pattern[str]] | None = None,
|
|
177
|
+
clock: Clock = SYSTEM_CLOCK,
|
|
176
178
|
) -> RunResult:
|
|
177
179
|
"""Spawn the agent subprocess and wait for exit or timeout.
|
|
178
180
|
|
|
@@ -213,7 +215,7 @@ def run(
|
|
|
213
215
|
env = {**os.environ, **env_extra, "PWD": str(work_dir)}
|
|
214
216
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
215
217
|
log_file = log_path.open("w", encoding="utf-8")
|
|
216
|
-
start =
|
|
218
|
+
start = clock.epoch()
|
|
217
219
|
last_progress_at = start
|
|
218
220
|
proc = subprocess.Popen(
|
|
219
221
|
argv,
|
|
@@ -246,13 +248,13 @@ def run(
|
|
|
246
248
|
try:
|
|
247
249
|
while True:
|
|
248
250
|
ret = proc.poll()
|
|
249
|
-
now =
|
|
251
|
+
now = clock.epoch()
|
|
250
252
|
if ret is not None:
|
|
251
253
|
duration = now - start
|
|
252
254
|
return RunResult(exit_code=ret, duration_s=duration, timed_out=False, pid=proc.pid)
|
|
253
255
|
if now - start > timeout_s:
|
|
254
|
-
_kill_pgroup(proc)
|
|
255
|
-
duration =
|
|
256
|
+
_kill_pgroup(proc, clock)
|
|
257
|
+
duration = clock.epoch() - start
|
|
256
258
|
exit_code = proc.returncode if proc.returncode is not None else -1
|
|
257
259
|
return RunResult(
|
|
258
260
|
exit_code=exit_code, duration_s=duration, timed_out=True, pid=proc.pid
|
|
@@ -276,8 +278,8 @@ def run(
|
|
|
276
278
|
on_grace_extended(live, ignored)
|
|
277
279
|
grace_extended_emitted = True
|
|
278
280
|
else:
|
|
279
|
-
_kill_pgroup(proc)
|
|
280
|
-
duration =
|
|
281
|
+
_kill_pgroup(proc, clock)
|
|
282
|
+
duration = clock.epoch() - start
|
|
281
283
|
exit_code = proc.returncode if proc.returncode is not None else -1
|
|
282
284
|
return RunResult(
|
|
283
285
|
exit_code=exit_code,
|
|
@@ -305,6 +307,6 @@ def run(
|
|
|
305
307
|
}
|
|
306
308
|
)
|
|
307
309
|
last_progress_at = now
|
|
308
|
-
|
|
310
|
+
clock.sleep(0.2)
|
|
309
311
|
finally:
|
|
310
312
|
log_file.close()
|
|
@@ -15,7 +15,6 @@ import shutil
|
|
|
15
15
|
import signal
|
|
16
16
|
import subprocess # noqa: TID251 — api uses systemctl + ssh, both subprocess
|
|
17
17
|
import sysconfig
|
|
18
|
-
import time
|
|
19
18
|
from collections.abc import Iterator, Sequence
|
|
20
19
|
from pathlib import Path
|
|
21
20
|
from typing import Any, Literal, TextIO
|
|
@@ -30,6 +29,7 @@ from agent_runner.api_types import (
|
|
|
30
29
|
ServiceStatus,
|
|
31
30
|
select_path,
|
|
32
31
|
)
|
|
32
|
+
from agent_runner.clock import SYSTEM_CLOCK
|
|
33
33
|
from agent_runner.config import Config, RuntimeConfig, load_config
|
|
34
34
|
from agent_runner.lifecycle import (
|
|
35
35
|
PIDFile,
|
|
@@ -323,13 +323,13 @@ def kill(project: str | Path) -> ServiceStatus:
|
|
|
323
323
|
if pid is None:
|
|
324
324
|
return status(project)
|
|
325
325
|
send_signal_to_pid(pid, signal.SIGTERM)
|
|
326
|
-
deadline =
|
|
326
|
+
deadline = SYSTEM_CLOCK.epoch() + 5
|
|
327
327
|
alive = True
|
|
328
|
-
while
|
|
328
|
+
while SYSTEM_CLOCK.epoch() < deadline:
|
|
329
329
|
alive = pid_alive(pid)
|
|
330
330
|
if not alive:
|
|
331
331
|
break
|
|
332
|
-
|
|
332
|
+
SYSTEM_CLOCK.sleep(0.1)
|
|
333
333
|
if alive:
|
|
334
334
|
send_signal_to_pid(pid, signal.SIGKILL)
|
|
335
335
|
return ServiceStatus(mode=ServiceMode.PID_FILE, active=alive, pid=pid)
|
|
@@ -448,6 +448,7 @@ def peek(
|
|
|
448
448
|
limit_type=throttle.classification,
|
|
449
449
|
agent=throttle.agent,
|
|
450
450
|
since_round=throttle.since_round,
|
|
451
|
+
phase=throttle.phase,
|
|
451
452
|
)
|
|
452
453
|
raw_service = status(project if project is not None else work_dir)
|
|
453
454
|
svc = dataclasses.replace(raw_service, rate_limit=rate_limit)
|
|
@@ -567,7 +568,7 @@ def _monitor_loop_iter(
|
|
|
567
568
|
log_dir=cfg.runtime.log_dir,
|
|
568
569
|
allowed_stop_names=cfg.monitor.auto_stop_on,
|
|
569
570
|
)
|
|
570
|
-
|
|
571
|
+
SYSTEM_CLOCK.sleep(interval_s)
|
|
571
572
|
|
|
572
573
|
|
|
573
574
|
def _tail_events_jsonl(
|
|
@@ -586,7 +587,6 @@ def _tail_events_jsonl(
|
|
|
586
587
|
appears, it is picked up from byte 0.
|
|
587
588
|
"""
|
|
588
589
|
import json as _json
|
|
589
|
-
import time as _time
|
|
590
590
|
|
|
591
591
|
seen_positions: dict[Path, int] = {}
|
|
592
592
|
if start_at_now:
|
|
@@ -620,7 +620,7 @@ def _tail_events_jsonl(
|
|
|
620
620
|
any_new = True
|
|
621
621
|
seen_positions[path] = f.tell()
|
|
622
622
|
if not any_new:
|
|
623
|
-
|
|
623
|
+
SYSTEM_CLOCK.sleep(poll_interval_s)
|
|
624
624
|
|
|
625
625
|
|
|
626
626
|
def _primary_prompt_file(cfg: Config) -> Path | None:
|
|
@@ -48,6 +48,7 @@ class RateLimitState:
|
|
|
48
48
|
limit_type: str
|
|
49
49
|
agent: str
|
|
50
50
|
since_round: int
|
|
51
|
+
phase: str = "" # which rotation phase is throttled ("" = no [phases])
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
@dataclass(frozen=True)
|
|
@@ -151,6 +152,7 @@ class TransientErrorState:
|
|
|
151
152
|
classification: str
|
|
152
153
|
agent: str
|
|
153
154
|
since_round: int
|
|
155
|
+
phase: str = "" # rotation phase the failing round ran under ("" = no [phases])
|
|
154
156
|
|
|
155
157
|
|
|
156
158
|
@dataclass(frozen=True)
|
{cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/builtin_plugins/claude_rate_limit.py
RENAMED
|
@@ -14,7 +14,6 @@ rate-limit detector was generalized to multi-classification in 0.1.23
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
import time
|
|
18
17
|
from pathlib import Path
|
|
19
18
|
from typing import Any
|
|
20
19
|
|
|
@@ -29,6 +28,7 @@ from agent_runner.builtin_plugins._constants import (
|
|
|
29
28
|
classify_transient_status,
|
|
30
29
|
json_events,
|
|
31
30
|
)
|
|
31
|
+
from agent_runner.clock import SYSTEM_CLOCK
|
|
32
32
|
from agent_runner.hooks import HookContext, register_post_round_hook
|
|
33
33
|
|
|
34
34
|
|
|
@@ -51,7 +51,9 @@ class ClaudeErrorDetector:
|
|
|
51
51
|
|
|
52
52
|
if parsed.get("transient_error"):
|
|
53
53
|
te = parsed["transient_error"]
|
|
54
|
-
emit_transient_error_detected(
|
|
54
|
+
emit_transient_error_detected(
|
|
55
|
+
ctx.log_dir, round_num=ctx.round_num, phase=ctx.phase or "", **te
|
|
56
|
+
)
|
|
55
57
|
|
|
56
58
|
if parsed.get("usage"):
|
|
57
59
|
emit_agent_usage_recorded(
|
|
@@ -175,7 +177,7 @@ def _classify_transient_error(
|
|
|
175
177
|
return {
|
|
176
178
|
"classification": "rate_limit_account",
|
|
177
179
|
"agent": "claude",
|
|
178
|
-
"reset_at_epoch": int(rate_limit_info.get("resetsAt",
|
|
180
|
+
"reset_at_epoch": int(rate_limit_info.get("resetsAt", SYSTEM_CLOCK.epoch() + 300)),
|
|
179
181
|
"raw": str((result_event or {}).get("result", ""))[:_RAW_CAP],
|
|
180
182
|
}
|
|
181
183
|
# rate_limit_event with null/other rateLimitType falls through to status-based
|
|
@@ -230,7 +232,7 @@ def _classify(classification: str, raw: str) -> dict[str, Any]:
|
|
|
230
232
|
return {
|
|
231
233
|
"classification": classification,
|
|
232
234
|
"agent": "claude",
|
|
233
|
-
"reset_at_epoch": int(
|
|
235
|
+
"reset_at_epoch": int(SYSTEM_CLOCK.epoch() + duration),
|
|
234
236
|
"raw": raw,
|
|
235
237
|
}
|
|
236
238
|
|
{cli_agent_runner-0.2.9 → cli_agent_runner-0.2.10}/agent_runner/builtin_plugins/codewhale.py
RENAMED
|
@@ -12,7 +12,6 @@ is added when a real rate-limit sample is captured.
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
-
import time
|
|
16
15
|
from pathlib import Path
|
|
17
16
|
from typing import Any
|
|
18
17
|
|
|
@@ -26,6 +25,7 @@ from agent_runner.builtin_plugins._constants import (
|
|
|
26
25
|
classify_transient_status,
|
|
27
26
|
json_events,
|
|
28
27
|
)
|
|
28
|
+
from agent_runner.clock import SYSTEM_CLOCK
|
|
29
29
|
from agent_runner.hooks import HookContext, register_post_round_hook
|
|
30
30
|
|
|
31
31
|
|
|
@@ -43,7 +43,10 @@ class CodewhaleErrorDetector:
|
|
|
43
43
|
parsed = _parse_codewhale_log(log_path)
|
|
44
44
|
if parsed.get("transient_error"):
|
|
45
45
|
emit_transient_error_detected(
|
|
46
|
-
ctx.log_dir,
|
|
46
|
+
ctx.log_dir,
|
|
47
|
+
round_num=ctx.round_num,
|
|
48
|
+
phase=ctx.phase or "",
|
|
49
|
+
**parsed["transient_error"],
|
|
47
50
|
)
|
|
48
51
|
if parsed.get("usage"):
|
|
49
52
|
emit_agent_usage_recorded(
|
|
@@ -92,7 +95,7 @@ def _parse_codewhale_log(log_path: Path) -> dict[str, Any]:
|
|
|
92
95
|
out["transient_error"] = {
|
|
93
96
|
"classification": classification,
|
|
94
97
|
"agent": "codewhale",
|
|
95
|
-
"reset_at_epoch": int(
|
|
98
|
+
"reset_at_epoch": int(SYSTEM_CLOCK.epoch() + duration),
|
|
96
99
|
"raw": str(error_event.get("error", "error"))[:_RAW_CAP],
|
|
97
100
|
}
|
|
98
101
|
return out
|
|
@@ -7,7 +7,6 @@ agent_usage_recorded event families without any agent-runner core changes.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import time
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
from typing import Any
|
|
13
12
|
|
|
@@ -21,6 +20,7 @@ from agent_runner.builtin_plugins._constants import (
|
|
|
21
20
|
classify_transient_status,
|
|
22
21
|
json_events,
|
|
23
22
|
)
|
|
23
|
+
from agent_runner.clock import SYSTEM_CLOCK
|
|
24
24
|
from agent_runner.hooks import HookContext, register_post_round_hook
|
|
25
25
|
|
|
26
26
|
|
|
@@ -38,7 +38,9 @@ class GeminiErrorDetector:
|
|
|
38
38
|
parsed = _parse_gemini_log(log_path)
|
|
39
39
|
if parsed.get("transient_error"):
|
|
40
40
|
te = parsed["transient_error"]
|
|
41
|
-
emit_transient_error_detected(
|
|
41
|
+
emit_transient_error_detected(
|
|
42
|
+
ctx.log_dir, round_num=ctx.round_num, phase=ctx.phase or "", **te
|
|
43
|
+
)
|
|
42
44
|
if parsed.get("usage"):
|
|
43
45
|
emit_agent_usage_recorded(
|
|
44
46
|
ctx.log_dir,
|
|
@@ -82,7 +84,7 @@ def _parse_gemini_log(log_path: Path) -> dict[str, Any]:
|
|
|
82
84
|
out["transient_error"] = {
|
|
83
85
|
"classification": classification,
|
|
84
86
|
"agent": "gemini",
|
|
85
|
-
"reset_at_epoch": int(
|
|
87
|
+
"reset_at_epoch": int(SYSTEM_CLOCK.epoch() + duration),
|
|
86
88
|
"raw": raw,
|
|
87
89
|
}
|
|
88
90
|
return out
|
|
@@ -30,7 +30,6 @@ retry record -- they arrive as plain text on stderr, e.g.
|
|
|
30
30
|
|
|
31
31
|
from __future__ import annotations
|
|
32
32
|
|
|
33
|
-
import time
|
|
34
33
|
from pathlib import Path
|
|
35
34
|
from typing import Any
|
|
36
35
|
|
|
@@ -41,6 +40,7 @@ from agent_runner.builtin_plugins._constants import (
|
|
|
41
40
|
classify_transient_status,
|
|
42
41
|
json_events,
|
|
43
42
|
)
|
|
43
|
+
from agent_runner.clock import SYSTEM_CLOCK
|
|
44
44
|
from agent_runner.hooks import HookContext, register_post_round_hook
|
|
45
45
|
|
|
46
46
|
|
|
@@ -62,7 +62,9 @@ class KimiErrorDetector:
|
|
|
62
62
|
return
|
|
63
63
|
transient_error = _parse_kimi_log(log_path)
|
|
64
64
|
if transient_error:
|
|
65
|
-
emit_transient_error_detected(
|
|
65
|
+
emit_transient_error_detected(
|
|
66
|
+
ctx.log_dir, round_num=ctx.round_num, phase=ctx.phase or "", **transient_error
|
|
67
|
+
)
|
|
66
68
|
|
|
67
69
|
|
|
68
70
|
def _parse_kimi_log(log_path: Path) -> dict[str, Any] | None:
|
|
@@ -85,7 +87,7 @@ def _parse_kimi_log(log_path: Path) -> dict[str, Any] | None:
|
|
|
85
87
|
return {
|
|
86
88
|
"classification": classification,
|
|
87
89
|
"agent": "kimi",
|
|
88
|
-
"reset_at_epoch": int(
|
|
90
|
+
"reset_at_epoch": int(SYSTEM_CLOCK.epoch() + _BACK_OFF_DEFAULTS[classification]),
|
|
89
91
|
"raw": str(retry_event.get("error_message", "retrying"))[:_RAW_CAP],
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -57,7 +57,6 @@ from __future__ import annotations
|
|
|
57
57
|
|
|
58
58
|
import json
|
|
59
59
|
import re
|
|
60
|
-
import time
|
|
61
60
|
from datetime import datetime
|
|
62
61
|
from itertools import islice
|
|
63
62
|
from pathlib import Path
|
|
@@ -74,6 +73,7 @@ from agent_runner.builtin_plugins._constants import (
|
|
|
74
73
|
classify_transient_status,
|
|
75
74
|
json_events,
|
|
76
75
|
)
|
|
76
|
+
from agent_runner.clock import SYSTEM_CLOCK
|
|
77
77
|
from agent_runner.hooks import HookContext, register_post_round_hook
|
|
78
78
|
|
|
79
79
|
_STATUS_RE = re.compile(r"^\s*(\d{3})\b")
|
|
@@ -118,8 +118,9 @@ class PiErrorDetector:
|
|
|
118
118
|
round_num=ctx.round_num,
|
|
119
119
|
classification=classification,
|
|
120
120
|
agent="pi",
|
|
121
|
-
reset_at_epoch=int(
|
|
121
|
+
reset_at_epoch=int(SYSTEM_CLOCK.epoch() + _BACK_OFF_DEFAULTS[classification]),
|
|
122
122
|
raw=str(error_text)[:_RAW_CAP],
|
|
123
|
+
phase=ctx.phase or "",
|
|
123
124
|
)
|
|
124
125
|
elif _is_auth_error(error_text):
|
|
125
126
|
# No transient classification on purpose — an auth failure is
|
|
@@ -18,10 +18,10 @@ import argparse
|
|
|
18
18
|
import json
|
|
19
19
|
import signal
|
|
20
20
|
import sys
|
|
21
|
-
import time
|
|
22
21
|
from datetime import UTC, datetime
|
|
23
22
|
from pathlib import Path
|
|
24
23
|
|
|
24
|
+
from agent_runner.clock import SYSTEM_CLOCK
|
|
25
25
|
from agent_runner.events import parse_iso_ms
|
|
26
26
|
|
|
27
27
|
# Sentinel for "user did not explicitly set --window" so we can detect
|
|
@@ -160,7 +160,7 @@ def cmd_events(args) -> int:
|
|
|
160
160
|
|
|
161
161
|
|
|
162
162
|
def _current_month_events_file(log_dir: Path) -> Path:
|
|
163
|
-
month =
|
|
163
|
+
month = SYSTEM_CLOCK.now_utc().strftime("%Y-%m")
|
|
164
164
|
return log_dir / f"events-{month}.jsonl"
|
|
165
165
|
|
|
166
166
|
|
|
@@ -308,6 +308,6 @@ def _tail_events(log_dir: Path, kind_set: set[str], since: datetime | None = Non
|
|
|
308
308
|
elif size < last_size:
|
|
309
309
|
# File truncated / rotated underneath us; reset
|
|
310
310
|
last_size = 0
|
|
311
|
-
|
|
311
|
+
SYSTEM_CLOCK.sleep(1.0)
|
|
312
312
|
except KeyboardInterrupt:
|
|
313
313
|
return 0
|