6be32a42dac8ea214aedb3ae97f7b9d01d183b5a
jnavarr5
  Tue Jul 28 17:25:30 2026 -0700
Scale the daily code review timeout with the commit count so large batches stop timing out, No RM.

diff --git src/utils/codeReviewAi.py src/utils/codeReviewAi.py
index 9a0dadd1d72..81f8a4fdbc4 100755
--- src/utils/codeReviewAi.py
+++ src/utils/codeReviewAi.py
@@ -31,30 +31,36 @@
 
 # Configuration
 REDMINE_URL = "https://redmine.gi.ucsc.edu"
 GIT_REPORTS_PATH = "/hive/groups/qa/git-reports-history"
 GIT_REPO_PATH = "/data/git/kent.git"
 OUTPUT_DIR = f"/hive/users/{getpass.getuser()}/codeReview"
 MLQ_CONF_PATH = os.path.expanduser("~/.hg.conf")
 DEFAULT_CC = "browser-code-reviews-group@ucsc.edu"
 DEFAULT_ALERT_EMAIL = "browserqa-group@ucsc.edu"
 GMAIL_TOKEN_PATH = os.path.expanduser("~/.gmail_token.json")
 GMAIL_CREDS_PATH = os.path.expanduser("~/.gmail_credentials.json")
 GMAIL_SCOPES = [
     'https://www.googleapis.com/auth/gmail.send',
 ]
 CLAUDE_CLI = os.path.expanduser('~/.local/bin/claude')
+# Daily reviews get a timeout scaled to the batch size. A flat 600s limit timed
+# out repeatedly on 8-11 commit days, and since the retry reused the same limit
+# those authors' commits went unreviewed with no later window to catch them.
+DAILY_TIMEOUT_BASE = 300
+DAILY_TIMEOUT_PER_COMMIT = 120
+DAILY_TIMEOUT_MAX = 2400
 
 def load_config():
     """Load API keys from ~/.hg.conf"""
     config = {}
     if not os.path.exists(MLQ_CONF_PATH):
         print(f"ERROR: Config file not found: {MLQ_CONF_PATH}")
         sys.exit(1)
 
     with open(MLQ_CONF_PATH, 'r') as f:
         for line in f:
             line = line.strip()
             if '=' in line and not line.startswith('#'):
                 key, value = line.split('=', 1)
                 config[key.strip()] = value.strip()
 
@@ -1362,33 +1368,36 @@
     print(f"Commits: {len(commits)}")
     print(f"{'='*60}")
 
     prompt = build_daily_review_prompt(author_name, commits, window_label)
 
     # Save prompt to log_dir for debugging (cleaned up on success)
     safe_name = re.sub(r'[^a-zA-Z0-9]', '_', author_name)
     suffix = file_suffix or datetime.now().strftime('%Y%m%d')
     temp_files = []
 
     prompt_file = os.path.join(log_dir, f".tmp_daily_prompt_{safe_name}_{suffix}.txt")
     with open(prompt_file, 'w') as f:
         f.write(prompt)
     temp_files.append(prompt_file)
     print(f"  Prompt saved to: {prompt_file}")
-    print(f"  Calling Claude CLI (this may take a few minutes)...")
 
-    raw_response = call_claude_cli(prompt, timeout=600, validator=validate_daily_review_output)
+    timeout = min(DAILY_TIMEOUT_MAX,
+                  DAILY_TIMEOUT_BASE + DAILY_TIMEOUT_PER_COMMIT * len(commits))
+    print(f"  Calling Claude CLI (timeout {timeout}s for {len(commits)} commit(s))...")
+    raw_response = call_claude_cli(prompt, timeout=timeout,
+                                   validator=validate_daily_review_output)
     error = detect_cli_failure(raw_response, validate_daily_review_output)
 
     # Save whatever we got back for debugging, even on failure.
     if raw_response:
         response_file = os.path.join(log_dir, f".tmp_daily_response_{safe_name}_{suffix}.txt")
         with open(response_file, 'w') as f:
             f.write(raw_response)
         temp_files.append(response_file)
 
     if error:
         print(f"  WARNING: review failed - {error}")
         response = f"DAILY CODE REVIEW - {author_name}\n\nError: {error}\n"
     else:
         response = raw_response
         # Strip any preamble before "DAILY CODE REVIEW"