Skip to content

[codex] Add runtime recovery policy#94

Merged
fakechris merged 1 commit into
mainfrom
codex/runtime-recovery-plane
May 15, 2026
Merged

[codex] Add runtime recovery policy#94
fakechris merged 1 commit into
mainfrom
codex/runtime-recovery-plane

Conversation

@fakechris

@fakechris fakechris commented May 15, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add an orthogonal runtime recovery policy for provider/connectivity failures observed in tmux output.
  • Detect rate limits with reset timestamps, malformed HTTP 200 gateway/proxy errors, and network disconnect/reset/timeout signals.
  • Add configurable quiet windows plus a GLM profile that reserves UTC+8 12:00-18:00 and uses a 5 hour fallback when no reset timestamp is available.

Behavior

  • Recovery injection uses retry to resume the current interrupted action.
  • It does not advance workflow nodes, change current_node, or consume workflow retry budget.
  • Foreground and daemon run paths both receive the configured runtime recovery policy.

Validation

  • pytest -q tests/test_runtime_recovery.py tests/test_config.py tests/test_sidecar_loop.py
  • pytest -q (1237 passed)

Summary by CodeRabbit

  • New Features

    • Added automatic runtime recovery system to detect and retry from transient errors and rate-limit conditions
    • Extended configuration with recovery settings including retry timing, quiet windows, and maximum attempts per run
  • Tests

    • Added comprehensive test coverage for runtime recovery detection and injection behavior

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@fakechris has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 38 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e10dc93-10a9-4f3d-8aa6-4d393111428f

📥 Commits

Reviewing files that changed from the base of the PR and between 7e2531e and 31acce2.

📒 Files selected for processing (6)
  • supervisor/app.py
  • supervisor/config.py
  • supervisor/daemon/server.py
  • supervisor/loop.py
  • supervisor/runtime_recovery.py
  • tests/test_runtime_recovery.py
📝 Walkthrough

Walkthrough

This PR implements a runtime recovery subsystem that automatically detects rate-limit resets and transient connectivity errors, schedules retry injection with configurable backoff timing and timezone-aware quiet windows, and integrates recovery into both daemon and foreground execution paths.

Changes

Runtime Recovery Implementation

Layer / File(s) Summary
Runtime recovery detection and policy management
supervisor/runtime_recovery.py
Introduces RuntimeRecoveryPolicy and RuntimeRecoveryObservation data models with default profiles. Implements detect_runtime_recovery() to classify rate-limit resets (with timestamp extraction) and transient errors, and next_allowed_recovery_at() to skip configured quiet windows using timezone-aware scheduling. Provides helper functions for reset-time parsing, quiet-window parsing, timezone normalization, and text signature generation.
Configuration schema and policy derivation
supervisor/config.py
Extends RuntimeConfig with new fields: runtime_recovery_enabled, runtime_recovery_profile, runtime_recovery_reset_timezone, runtime_recovery_reset_grace_seconds, runtime_recovery_transient_delay_seconds, runtime_recovery_rate_limit_fallback_delay_seconds, runtime_recovery_quiet_windows, runtime_recovery_max_attempts_per_run. Adds provider-retry back-compat aliases. Extends coerce_config_value() to parse boolean strings from environment, and adds runtime_recovery_policy() method to merge configured and aliased values with profile defaults. Updates default_config_yaml() template with runtime recovery block.
Sidecar loop injection and retry handling
supervisor/loop.py
Adds runtime_recovery_policy parameter to SupervisorLoop.__init__() with default RuntimeRecoveryPolicy(). In _run_sidecar_inner, initializes retry tracking counters. When checkpoint parsing yields no checkpoints, detects recovery conditions, records scheduling events with attempt bounds, waits until scheduled retry time, builds recovery HandoffInstruction via _build_runtime_recovery_instruction(), injects it into the terminal, records injection events, and resumes the loop.
Manager and daemon wiring
supervisor/app.py, supervisor/daemon/server.py
Foreground cmd_run_foreground() passes runtime_recovery_policy=config.runtime_recovery_policy() to AutoInterventionManager. Daemon DaemonServer._run_worker() passes it to loop.run_sidecar().
Test coverage for detection, scheduling, and injection
tests/test_runtime_recovery.py
Validates detection of Claude 429 rate-limit reset (Chinese text in Asia/Shanghai with configured grace), English "expires at" variants, transient HTTP 200 errors with short retry delays, and glm profile midday reservation behavior. Tests quiet-window skipping with UTC+8 high-watermark boundaries. Integration test verifies sidecar injects retry instruction for malformed HTTP 200 and completes in ATTACHED or RUNNING state.

Sequence Diagrams

sequenceDiagram
  participant TextOutput as Terminal Output
  participant Detector as detect_runtime_recovery
  participant Extractor as _extract_reset_time
  participant WindowCalc as next_allowed_recovery_at
  participant PolicyCfg as RuntimeRecoveryPolicy
  TextOutput->>Detector: error/rate-limit text
  Detector->>Detector: classify kind
  Detector->>Extractor: scan for reset timestamp
  Extractor->>PolicyCfg: read reset_grace_seconds
  Extractor-->>Detector: candidate retry_at
  Detector->>WindowCalc: advance past quiet windows
  WindowCalc->>PolicyCfg: read quiet_windows
  WindowCalc-->>Detector: clamped retry_at
  Detector-->>TextOutput: RuntimeRecoveryObservation
Loading
sequenceDiagram
  participant LoopExecution as Loop._run_sidecar_inner
  participant DetectModule as Detector
  participant BuildFn as _build_recovery_instruction
  participant TerminalIO as Terminal
  participant EventLog as SessionEvents
  LoopExecution->>DetectModule: terminal text
  DetectModule-->>LoopExecution: RuntimeRecoveryObservation
  LoopExecution->>EventLog: record scheduled
  LoopExecution->>BuildFn: construct HandoffInstruction
  BuildFn-->>LoopExecution: recovery instruction
  LoopExecution->>TerminalIO: inject instruction
  LoopExecution->>EventLog: record injected
  LoopExecution->>EventLog: continue iteration
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • fakechris/thin-supervisor#78: Both PRs modify the sidecar loop's recovery-handling flow in _run_sidecar_inner, with PR #94 adding config-driven retry injection for detected errors while PR #78 adds a boot-time RECOVERY_NEEDED pause path—overlapping in the same recovery code area.

Poem

A rabbit hops through errors with grace,
Rate limits? Quiet windows in place!
Timezone-aware, it retries with flair,
Each recovery scheduled with utmost care.
Config-driven bounces, the supervisor takes flight! 🐰⏰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[codex] Add runtime recovery policy' accurately reflects the main change: introducing a new runtime recovery policy feature across multiple supervisor components for handling provider/connectivity failures.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/runtime-recovery-plane

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a runtime recovery system to handle provider-side failures, such as rate limits and transient connectivity issues, by automatically scheduling and injecting retries. Key changes include new configuration options for recovery policies, a dedicated runtime_recovery module for error detection, and integration into the supervisor's sidecar loop. Review feedback identified a critical logic bug where the recovery timestamp could slide forward indefinitely, potential log spamming during exhausted attempts, and a crash risk when parsing invalid timezone strings from user configuration.

Comment thread supervisor/loop.py Outdated
Comment on lines +1063 to +1091
attempts = recovery_attempts.get(recovery.signature, 0)
if recovery_total_attempts < self.runtime_recovery_policy.max_attempts_per_run:
scheduled_recovery = recovery
if scheduled_recovery_signature != recovery.signature:
scheduled_recovery_signature = recovery.signature
self.store.append_session_event(
state.run_id,
"runtime_recovery_scheduled",
{
"kind": recovery.kind,
"reason": recovery.reason,
"retry_at": recovery.retry_at.isoformat(),
"attempt": recovery_total_attempts + 1,
"max_attempts": self.runtime_recovery_policy.max_attempts_per_run,
},
)
self.store.save(state)
else:
self.store.append_session_event(
state.run_id,
"runtime_recovery_exhausted",
{
"kind": recovery.kind,
"reason": recovery.reason,
"attempts": recovery_total_attempts,
"max_attempts": self.runtime_recovery_policy.max_attempts_per_run,
},
)
self.store.save(state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This block has several issues:

  1. Redundant Assignment: attempts is assigned but never used here (it is correctly retrieved later at line 1094).
  2. Sliding Fallback Delay: scheduled_recovery is updated every tick. If the recovery uses a fallback delay (relative to now), the retry_at timestamp will slide forward every iteration as long as the error text persists, preventing the recovery from ever triggering.
  3. Log Spamming: If recovery attempts are exhausted, the runtime_recovery_exhausted event is appended to the session log every single tick.

All of these can be resolved by only updating the state and logging when the error signature changes.

                    if recovery_total_attempts < self.runtime_recovery_policy.max_attempts_per_run:
                        if scheduled_recovery is None or scheduled_recovery_signature != recovery.signature:
                            scheduled_recovery = recovery
                            scheduled_recovery_signature = recovery.signature
                            self.store.append_session_event(
                                state.run_id,
                                "runtime_recovery_scheduled",
                                {
                                    "kind": recovery.kind,
                                    "reason": recovery.reason,
                                    "retry_at": recovery.retry_at.isoformat(),
                                    "attempt": recovery_total_attempts + 1,
                                    "max_attempts": self.runtime_recovery_policy.max_attempts_per_run,
                                },
                            )
                            self.store.save(state)
                    elif scheduled_recovery_signature != recovery.signature:
                        scheduled_recovery_signature = recovery.signature
                        self.store.append_session_event(
                            state.run_id,
                            "runtime_recovery_exhausted",
                            {
                                "kind": recovery.kind,
                                "reason": recovery.reason,
                                "attempts": recovery_total_attempts,
                                "max_attempts": self.runtime_recovery_policy.max_attempts_per_run,
                            },
                        )
                        self.store.save(state)

Comment thread supervisor/loop.py Outdated
if not checkpoints:
recovery = detect_runtime_recovery(
text,
now=datetime.now(timezone.utc),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The variable name now is already used in this scope as a float (from time.monotonic() at line 1013). While using it as a keyword argument name here is technically valid, it makes the code harder to read, especially since now is used again as a float at line 1127. Consider using a more descriptive name for the datetime object or letting the function use its default.

Comment thread supervisor/runtime_recovery.py Outdated
return timezone(sign * timedelta(hours=hours, minutes=minutes))
if normalized.upper() == "UTC":
return timezone.utc
return ZoneInfo(normalized)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

ZoneInfo(normalized) will raise a zoneinfo.ZoneInfoNotFoundError if the provided timezone name is invalid. Since this value can come from user configuration, it should be handled gracefully to avoid crashing the supervisor daemon when it attempts to parse a rate limit error.

Suggested change
return ZoneInfo(normalized)
try:
return ZoneInfo(normalized)
except Exception:
return timezone.utc

@fakechris

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@fakechris
fakechris force-pushed the codex/runtime-recovery-plane branch from 7e2531e to 976ed7e Compare May 15, 2026 03:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@supervisor/config.py`:
- Around line 277-280: The current construction of RuntimeRecoveryPolicy treats
provider_retry_* as subordinate booleans/defaults rather than fallbacks; change
the assignments so each runtime_recovery_* field is used if explicitly set (not
just truthy) and otherwise falls back to provider_retry_*. Specifically, when
creating policy_with_profile(RuntimeRecoveryPolicy(...)) compute enabled =
(self.runtime_recovery_enabled if self.runtime_recovery_enabled is not None else
self.provider_retry_enabled), and similarly for reset_timezone and profile use
(self.runtime_recovery_reset_timezone if self.runtime_recovery_reset_timezone is
not None else self.provider_retry_reset_timezone) and
(self.runtime_recovery_profile if self.runtime_recovery_profile is not None else
self.provider_retry_profile) so legacy provider_retry_* acts as a true fallback
rather than a kill-switch.

In `@supervisor/loop.py`:
- Around line 918-920: The scheduled_recovery state (scheduled_recovery and
scheduled_recovery_signature) must be cleared when a real checkpoint for the run
is accepted to avoid injecting stale recoveries; update the checkpoint
acceptance code path to set scheduled_recovery = None and
scheduled_recovery_signature = "" immediately after the first accepted
checkpoint for that run (i.e., in the checkpoint acceptance/commit handler where
a run transitions to healthy), and apply the same clear logic in the other
checkpoint-handling block referenced (the second checkpoint acceptance path
covering the alternative flow).
- Around line 1062-1068: detect_runtime_recovery() is repeatedly sliding the
retry deadline by overwriting scheduled_recovery on every poll; change the logic
so you only set scheduled_recovery and append the session event when this is the
first time seeing this recovery signature (i.e., scheduled_recovery_signature !=
recovery.signature) or when no scheduled_recovery exists, and skip updating
scheduled_recovery/retry_at when the incoming recovery.signature matches the
already scheduled_recovery_signature to avoid moving retry_at forward for the
same failure; keep the existing checks for recovery_total_attempts and attempts
from recovery_attempts, but ensure scheduled_recovery,
scheduled_recovery_signature and the call to self.store.append_session_event are
only changed on signature transitions.

In `@supervisor/runtime_recovery.py`:
- Around line 110-114: The issue is that timestamp-only reset messages parsed by
_extract_reset_time (e.g., "expires at ...", "限额将在 ... 重置") are ignored because
the keyword check gates recovery; change the logic so you call
_extract_reset_time(text, policy=policy) first and, if it returns a datetime,
set retry_at to that (then pass through next_allowed_recovery_at), otherwise
fall back to the existing keyword check and rate_limit_fallback_delay_seconds
behavior; update the branch involving "429"/"rate limit"/"使用上限" to only handle
fallback when _extract_reset_time returned None, keeping function names
_extract_reset_time and next_allowed_recovery_at and variables retry_at, policy,
now unchanged for easy location.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d3f66db7-5839-49e8-a44b-b36ea5b6a8c2

📥 Commits

Reviewing files that changed from the base of the PR and between ad96e71 and 7e2531e.

📒 Files selected for processing (6)
  • supervisor/app.py
  • supervisor/config.py
  • supervisor/daemon/server.py
  • supervisor/loop.py
  • supervisor/runtime_recovery.py
  • tests/test_runtime_recovery.py

Comment thread supervisor/config.py Outdated
Comment thread supervisor/loop.py
Comment thread supervisor/loop.py
Comment thread supervisor/runtime_recovery.py
@fakechris
fakechris force-pushed the codex/runtime-recovery-plane branch from 976ed7e to 31acce2 Compare May 15, 2026 04:05
@fakechris
fakechris merged commit 03c4b19 into main May 15, 2026
5 checks passed
@fakechris
fakechris deleted the codex/runtime-recovery-plane branch May 15, 2026 04:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant