Repository navigation
Fix: Implement reset time parsing for auto-continue when limit is reached #19 - #35
Conversation
Added functionality to parse and handle reset time for auto-continue based on agent response when Limit is reached for Claude Code SDK
📝 WalkthroughWalkthroughThe agent now parses timezone-aware reset times from "limit reached" responses and replaces the fixed continuation delay with a computed, time-zone-aware sleep duration derived from the parsed target time; falls back to the default delay on parse failure. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In @agent.py:
- Around line 19-23: The Windows encoding wrapper currently assumes sys.stdout
and sys.stderr expose a .buffer attribute and will raise if they don’t; update
the conditional in the sys.platform == "win32" block to first verify
hasattr(sys.stdout, "buffer") and hasattr(sys.stderr, "buffer") (or wrap the
TextIOWrapper creation in a try/except AttributeError) and only replace streams
when the buffer attribute exists, otherwise skip the rewrapping to avoid
import-time failures in environments/IDEs that replace sys.stdout/sys.stderr.
- Around line 200-255: The limit-reset parsing is brittle and can produce
arbitrarily long sleeps; update the check on the agent response to detect "limit
reached" more flexibly (e.g., case-insensitive word-boundary search rather than
response.lower().strip().startswith(...)), replace the regex with a more
tolerant pattern like
(?i)\bresets(?:\s+at)?\s+(\d+)(?::(\d+))?\s*(am|pm)\s*\(([^)]+)\) to capture
common variants, strip whitespace from the captured tz_name before calling
ZoneInfo (tz_name = match.group(4).strip()), and after computing delay_seconds
clamp it into a safe range (e.g., delay_seconds = max(0, min(delay_seconds,
24*3600))) so print_progress_summary, AUTO_CONTINUE_DELAY_SECONDS and the
asyncio.sleep call use a bounded delay.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
agent.py
🧰 Additional context used
🧬 Code graph analysis (1)
agent.py (1)
progress.py (1)
print_progress_summary(211-223)
| # Fix Windows console encoding for Unicode characters (emoji, etc.) | ||
| # Without this, print() crashes when Claude outputs emoji like ✅ | ||
| if sys.platform == "win32": | ||
| sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') | ||
| sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') | ||
| sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") | ||
| sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") |
There was a problem hiding this comment.
Guard Win32 encoding wrapper for environments without .buffer.
Some runners/IDEs replace sys.stdout/sys.stderr with objects that may not have .buffer; this would raise on Windows at import time.
Proposed fix (defensive guard)
if sys.platform == "win32":
- sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
- sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
+ if hasattr(sys.stdout, "buffer"):
+ sys.stdout = io.TextIOWrapper(
+ sys.stdout.buffer, encoding="utf-8", errors="replace"
+ )
+ if hasattr(sys.stderr, "buffer"):
+ sys.stderr = io.TextIOWrapper(
+ sys.stderr.buffer, encoding="utf-8", errors="replace"
+ )🤖 Prompt for AI Agents
In @agent.py around lines 19 - 23, The Windows encoding wrapper currently
assumes sys.stdout and sys.stderr expose a .buffer attribute and will raise if
they don’t; update the conditional in the sys.platform == "win32" block to first
verify hasattr(sys.stdout, "buffer") and hasattr(sys.stderr, "buffer") (or wrap
the TextIOWrapper creation in a try/except AttributeError) and only replace
streams when the buffer attribute exists, otherwise skip the rewrapping to avoid
import-time failures in environments/IDEs that replace sys.stdout/sys.stderr.
Applied Fixes
More flexible string matching: Changed from response.lower().strip().startswith("limit reached") to "limit reached" in response.lower() to handle cases where the message has prefix text or variations in whitespace.
Improved regex pattern: Updated to r"(?i)\bresets(?:\s+at)?\s+(\d+)(?::(\d+))?\s*(am|pm)\s*\(([^)]+)\)" which now handles:
Optional "at" after "resets" (e.g., "resets at 3pm" or "resets 3pm")
Flexible whitespace around components
Word boundaries to prevent partial matches
Timezone sanitization: Added .strip() to tz_name = match.group(4).strip() to remove any leading/trailing whitespace that could cause ZoneInfo() to fail.
Safety clamp: Added delay_seconds = min(delta.total_seconds(), 24 * 60 * 60) to ensure the delay never exceeds 24 hours, preventing the agent from being stuck waiting for extremely long periods.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In @agent.py:
- Around line 208-223: The regex and conversion allow invalid 12-hour values;
after matching in the block that sets hour/minute/period/tz_name (the match
handling where you convert to 24-hour format), validate that hour is between 1
and 12 and minute is between 0 and 59 (or tighten the regex to enforce those
ranges) and if the values are invalid, handle gracefully (e.g., skip parsing,
log/warn, or return None) before doing the 24-hour conversion or calling
datetime.replace to avoid producing invalid datetimes.
- Line 15: The module-level import of ZoneInfo in agent.py will raise
ImportError on Python <3.9; either protect it by wrapping the import in a
try/except and fall back to the backports.zoneinfo package or a
pytz/dateutil-based replacement (setting a ZoneInfo alias used throughout the
module), or instead declare and enforce Python>=3.9 in
pyproject.toml/setup.py/README so the import is guaranteed to succeed; update
imports and any uses of ZoneInfo accordingly to avoid import-time crashes.
🧹 Nitpick comments (3)
agent.py (3)
240-241: Consider improving error logging for debugging.The generic error message doesn't include details about what was being parsed. Consider logging the matched values or the original response excerpt to aid debugging when reset time parsing fails.
💡 Suggested enhancement
except Exception as e: - print(f"Error parsing reset time: {e}, using default delay") + print(f"Error parsing reset time (hour={hour}, minute={minute}, tz={tz_name}): {e}, using default delay")
243-255: Consider adding an upper bound check for delay_seconds.While the parsed reset time logic handles date rollover correctly, there's no upper bound on
delay_seconds. If the reset time is far in the future (e.g., 23+ hours), the agent will sleep for an extended period. Consider whether a maximum delay (e.g., 24 hours) should be enforced to prevent unexpectedly long waits.⏱️ Suggested safeguard
delta = target - now delay_seconds = delta.total_seconds() + + # Cap delay at 24 hours to avoid excessive waiting + MAX_DELAY_SECONDS = 24 * 60 * 60 # 24 hours + if delay_seconds > MAX_DELAY_SECONDS: + print(f"Computed delay ({delay_seconds:.0f}s) exceeds maximum, capping at {MAX_DELAY_SECONDS}s") + delay_seconds = MAX_DELAY_SECONDS + target_time_str = target.strftime("%B %d, %Y at %I:%M %p %Z")Alternatively, if extended waits are intentional, this suggestion can be ignored.
253-253: Optional: Remove redundant flush.Since the print statements above (lines 244-251) already use
flush=True, the explicitsys.stdout.flush()call on line 253 is redundant. While harmless, you could remove it for cleaner code.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
agent.py
🧰 Additional context used
🧬 Code graph analysis (1)
agent.py (1)
progress.py (1)
print_progress_summary(211-223)
🔇 Additional comments (1)
agent.py (1)
201-202: LGTM!Good defensive initialization—ensures both variables have safe defaults before the conditional parsing logic.
| from datetime import datetime, timedelta | ||
| from pathlib import Path | ||
| from typing import Optional | ||
| from zoneinfo import ZoneInfo |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check Python version requirements in project config files
echo "=== Checking for Python version specifications ==="
# Search for version specs in common project config files
fd -type f \( -name "setup.py" -o -name "setup.cfg" -o -name "pyproject.toml" -o -name "requirements*.txt" -o -name ".python-version" \) -exec cat {} \;
echo ""
echo "=== Checking for version checks in codebase ==="
# Look for sys.version_info checks
rg "sys\.version_info|python_requires" -A 3 -B 1
echo ""
echo "=== Checking for ZoneInfo usage and fallbacks ==="
# Check for ZoneInfo imports and any try/except handling
rg "zoneinfo|ZoneInfo" -B 2 -A 2
echo ""
echo "=== Checking for pytz or dateutil imports (fallback mechanisms) ==="
rg "pytz|dateutil\.tz" -B 1 -A 1Repository: leonvanzyl/autocoder
Length of output: 859
🏁 Script executed:
#!/bin/bash
# List all files in repo root to see what config exists
echo "=== Files in repository root ==="
ls -la
echo ""
echo "=== Context around ZoneInfo usage in agent.py ==="
rg -B 5 -A 10 "tz = ZoneInfo"
echo ""
echo "=== Check README or docs for version info ==="
find . -maxdepth 2 -type f \( -iname "README*" -o -iname "*.md" \) -exec grep -l -i "python" {} \;Repository: leonvanzyl/autocoder
Length of output: 150
🏁 Script executed:
#!/bin/bash
# Read agent.py directly to see context around ZoneInfo
echo "=== agent.py lines 1-30 ==="
head -30 agent.py
echo ""
echo "=== agent.py lines 195-260 (around ZoneInfo usage) ==="
sed -n '195,260p' agent.pyRepository: leonvanzyl/autocoder
Length of output: 3748
Protect ZoneInfo import for Python < 3.9 compatibility or document minimum version requirement.
ZoneInfo (line 15) requires Python 3.9+, but this minimum version is not documented in project config, and the import is unprotected at module level. The code will immediately fail to import on Python 3.8 or earlier. Either add a version check/fallback (e.g., using pytz or dateutil.tz for older versions) or explicitly document Python 3.9+ as the minimum requirement in setup.py/pyproject.toml/README.
🤖 Prompt for AI Agents
In @agent.py at line 15, The module-level import of ZoneInfo in agent.py will
raise ImportError on Python <3.9; either protect it by wrapping the import in a
try/except and fall back to the backports.zoneinfo package or a
pytz/dateutil-based replacement (setting a ZoneInfo alias used throughout the
module), or instead declare and enforce Python>=3.9 in
pyproject.toml/setup.py/README so the import is guaranteed to succeed; update
imports and any uses of ZoneInfo accordingly to avoid import-time crashes.
| match = re.search( | ||
| r"resets (\d+)(?::(\d+))?(am|pm) \(([^)]+)\)", | ||
| response, | ||
| re.IGNORECASE, | ||
| ) | ||
| if match: | ||
| hour = int(match.group(1)) | ||
| minute = int(match.group(2)) if match.group(2) else 0 | ||
| period = match.group(3).lower() | ||
| tz_name = match.group(4) | ||
|
|
||
| # Convert to 24-hour format | ||
| if period == "pm" and hour != 12: | ||
| hour += 12 | ||
| elif period == "am" and hour == 12: | ||
| hour = 0 |
There was a problem hiding this comment.
Validate hour range to prevent invalid time conversions.
The regex \d+ captures any number for the hour, but valid 12-hour format hours are 1-12. If the agent returns an invalid hour (e.g., "resets 25pm"), the conversion logic will produce incorrect values (hour=37 in this case), which may cause datetime.replace() to fail or produce unexpected results.
🛡️ Proposed fix: Add hour validation
if match:
hour = int(match.group(1))
minute = int(match.group(2)) if match.group(2) else 0
period = match.group(3).lower()
tz_name = match.group(4)
+
+ # Validate hour is in valid 12-hour range
+ if not (1 <= hour <= 12):
+ print(f"Invalid hour {hour} in reset time, using default delay")
+ continue # Skip to the else block (default delay)
# Convert to 24-hour format
if period == "pm" and hour != 12:
hour += 12
elif period == "am" and hour == 12:
hour = 0Alternatively, you could tighten the regex pattern itself:
- r"resets (\d+)(?::(\d+))?(am|pm) \(([^)]+)\)",
+ r"resets (1[0-2]|[1-9])(?::([0-5][0-9]))?(am|pm) \(([^)]+)\)",This pattern ensures hour is 1-12 and minute (if present) is 00-59.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @agent.py around lines 208 - 223, The regex and conversion allow invalid
12-hour values; after matching in the block that sets hour/minute/period/tz_name
(the match handling where you convert to 24-hour format), validate that hour is
between 1 and 12 and minute is between 0 and 59 (or tighten the regex to enforce
those ranges) and if the values are invalid, handle gracefully (e.g., skip
parsing, log/warn, or return None) before doing the 24-hour conversion or
calling datetime.replace to avoid producing invalid datetimes.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
agent.py (2)
236-238: Consider extracting the 24-hour limit as a named constant.The magic number
24 * 60 * 60(86400 seconds) is used for clamping the delay. Extracting this to a module-level constant would improve readability.♻️ Proposed refactor
At the module level (near line 35 with other constants):
# Configuration AUTO_CONTINUE_DELAY_SECONDS = 3 +MAX_DELAY_SECONDS = 24 * 60 * 60 # 24 hoursThen update the usage:
- delay_seconds = min( - delta.total_seconds(), 24 * 60 * 60 - ) # Clamp to 24 hours max + delay_seconds = min(delta.total_seconds(), MAX_DELAY_SECONDS)
224-242: Consider refactoring parse logic into a separate function.The reset time parsing logic (lines 208-242) could be extracted into a dedicated function for improved testability and maintainability. This would allow unit testing of timezone conversion, 12-hour format handling, and edge cases independently.
♻️ Example refactor
def parse_reset_time(response: str) -> Optional[tuple[float, str]]: """ Parse reset time from agent response. Returns: (delay_seconds, target_time_str) if parsed successfully, None otherwise """ match = re.search( r"(?i)\bresets(?:\s+at)?\s+(\d+)(?::(\d+))?\s*(am|pm)\s*\(([^)]+)\)", response, ) if not match: return None hour = int(match.group(1)) minute = int(match.group(2)) if match.group(2) else 0 period = match.group(3).lower() tz_name = match.group(4).strip() # Convert to 24-hour format if period == "pm" and hour != 12: hour += 12 elif period == "am" and hour == 12: hour = 0 try: tz = ZoneInfo(tz_name) now = datetime.now(tz) target = now.replace(hour=hour, minute=minute, second=0, microsecond=0) if target <= now: target += timedelta(days=1) delta = target - now delay_seconds = min(delta.total_seconds(), 24 * 60 * 60) target_time_str = target.strftime("%B %d, %Y at %I:%M %p %Z") return (delay_seconds, target_time_str) except Exception as e: print(f"Error parsing reset time: {e}") return NoneThen in the main flow:
if "limit reached" in response.lower(): print("Claude Agent SDK indicated limit reached.") parsed = parse_reset_time(response) if parsed: delay_seconds, target_time_str = parsed # else: use defaults already set
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
agent.py
🔇 Additional comments (5)
agent.py (5)
201-205: LGTM! Flexible limit detection.The case-insensitive substring check for "limit reached" is appropriately flexible, allowing the phrase to appear anywhere in the response rather than requiring a specific position or format.
208-216: Regex pattern effectively captures reset time components.The pattern correctly handles:
- Optional "at" keyword
- Flexible whitespace
- Optional minutes (defaulting to 0)
- Case-insensitive matching
- Timezone name extraction
219-222: 12-hour to 24-hour conversion logic is correct.The conversion properly handles:
- PM times: adds 12 except for 12 PM
- AM times: converts 12 AM (midnight) to 0
244-256: Excellent user feedback and output handling.The conditional messaging provides clear feedback to users about whether a specific reset time was detected or if the default delay is being used. The
sys.stdout.flush()call ensures the pause message is displayed immediately before sleeping.
15-15: Python 3.11 is already required; Windows tzdata concerns are handled by existing error handling.The project's
pyproject.tomlspecifiespython_version = "3.11", which exceeds the Python 3.9+ minimum required forZoneInfo. Additionally, the code at line 225 already wrapsZoneInfo(tz_name)in a try-except block (lines 42-43) that gracefully catches anyZoneInfoNotFoundErroron Windows systems lacking tzdata and falls back to the default auto-continue delay. No additional fallback imports, dependency declarations, or documentation changes are needed.
|
Thank you @coreycauble |
- Add POST /api/providers endpoint for creating AI provider configurations - Implement AES-256-GCM encryption for API key storage - Add validation for required fields (id, name, apiKey) - Return 201 status on successful creation - Mask API key in response (show first 4 and last 4 chars) - Add ValidationError handling with 400 status - Export decryptApiKey for future use (provider connection testing) Feature AutoForgeAI#35: Add provider via API - verified end-to-end Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add AcceptanceGate class with evaluate(run, acceptance_spec) method - Implement gate_mode support (all_pass, any_pass) - Add required validator flag enforcement - Build acceptance_results array with per-validator outcomes - Update AgentRun with final_verdict and acceptance_results - Add GateResult dataclass for gate evaluation results - Export AcceptanceGate and GateResult from api package - Add comprehensive test suite (36 tests, all passing) - Add verification script (11 steps, all passing) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix: Implement reset time parsing for auto-continue when limit is reached AutoForgeAI#19
Added functionality to parse and handle reset time for auto-continue based on agent response when Limit is reached for Claude Code SDK
Summary by CodeRabbit
New Features
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.