Claude code 重试策略修改
donnad
2026-06-24 23:21
1
Claude code api失败重试策略为exponential backoff,并且v2.1.186开始重试次数上限为15次,见 Environment variables - Claude Code Docs。
有些公益站使用不稳定,Claude code现在的策略导致等待时间久,并且长任务会中断,所以用ai修改下Claude code二进制文件,实现为1s间隔重试,重试次数最大99次。
效果

执行patch(仅在linux环境验证过v2.1.186,v2.1.187)
python3 patch-retry.py
脚本
#!/usr/bin/env python3
"""
patch-retry.py — Patch Claude Code binary to:
1. Remove the 15-retry cap on CLAUDE_CODE_MAX_RETRIES
2. Replace exponential backoff with fixed 1s interval
3. Patch the Anthropic SDK's built-in retry backoff
4. Lower rate-limit fallback delays
Usage:
sudo python3 patch-retry.py [--dry-run] [--restore]
Options:
--dry-run Show what would be changed without modifying the binary
--restore Restore the original binary from backup
Environment variables (after patching):
CLAUDE_CODE_MAX_RETRIES=100 — Max retry attempts (no longer capped at 15)
CLAUDE_CODE_RETRY_INTERVAL_MS=1 — Fixed 1s delay for rate-limit retries
Version-agnostic: This script dynamically discovers minified variable names
by searching for code structure patterns (e.g. "clamped to ${VAR}" near
"CLAUDE_CODE_MAX_RETRIES") rather than hardcoding variable names. This
allows it to work across versions where minification produces different names.
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
def find_binary() -> str:
"""Find the Claude Code binary path."""
# Try `which claude` first
try:
result = subprocess.run(
["which", "claude"], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
path = os.path.realpath(result.stdout.strip())
if os.path.isfile(path):
return path
except Exception:
pass
# Fallback: look in npm global packages
try:
result = subprocess.run(
["npm", "root", "-g"], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
npm_root = result.stdout.strip()
candidates = [
os.path.join(npm_root, "@anthropic-ai/claude-code/bin/claude.exe"),
os.path.join(npm_root, "@anthropic-ai/claude-code-linux-x64/claude"),
os.path.join(npm_root, "@anthropic-ai/claude-code-linux-arm64/claude"),
os.path.join(npm_root, "@anthropic-ai/claude-code-darwin-arm64/claude"),
os.path.join(npm_root, "@anthropic-ai/claude-code-darwin-x64/claude"),
]
for c in candidates:
if os.path.isfile(c):
return os.path.realpath(c)
except Exception:
pass
print("ERROR: Could not find Claude Code binary. Is it installed?", file=sys.stderr)
print("Try: npm install -g @anthropic-ai/claude-code", file=sys.stderr)
sys.exit(1)
def find_all(data: bytes, pattern: bytes) -> list[int]:
"""Find all offsets of a byte pattern in data."""
offsets = []
start = 0
while True:
idx = data.find(pattern, start)
if idx == -1:
break
offsets.append(idx)
start = idx + 1
return offsets
def find_nearest(data: bytes, pattern: bytes, ref_offset: int, max_dist: int) -> int | None:
"""Find the offset of pattern closest to ref_offset, within max_dist."""
offsets = find_all(data, pattern)
best = None
best_dist = max_dist
for off in offsets:
dist = abs(off - ref_offset)
if dist < best_dist:
best_dist = dist
best = off
return best
def apply_byte_patch(data: bytearray, desc: str, search: bytes, replace: bytes,
stats: dict, hint_offset: int | None = None,
max_dist: int = 0) -> bytearray:
"""Apply a single search→replace byte patch. Returns modified data.
If hint_offset is provided, finds the search pattern nearest to that offset
within max_dist. Otherwise, uses the first occurrence.
"""
if len(search) != len(replace):
print(f" SKIP: {desc} — byte length mismatch ({len(search)} vs {len(replace)})", file=sys.stderr)
stats["failed"] += 1
return data
if hint_offset is not None:
offset = find_nearest(data, search, hint_offset, max_dist)
if offset is None:
print(f" WARN: Could not find pattern near hint for: {desc}", file=sys.stderr)
stats["failed"] += 1
return data
else:
offsets = find_all(data, search)
if not offsets:
print(f" WARN: Could not find pattern for: {desc}", file=sys.stderr)
stats["failed"] += 1
return data
offset = offsets[0]
# Verify the bytes at the offset match
actual = data[offset:offset + len(search)]
if actual != search:
print(f" SKIP: {desc} — byte mismatch at offset {offset}", file=sys.stderr)
print(f" Expected: {search.hex()}", file=sys.stderr)
print(f" Actual: {actual.hex()}", file=sys.stderr)
stats["failed"] += 1
return data
print(f" ✓ {desc} @ offset {offset}")
data[offset:offset + len(replace)] = replace
stats["applied"] += 1
return data
# ─── Dynamic pattern discovery ────────────────────────────────────────────────
# These functions discover minified variable names by searching for code
# structure patterns that are stable across versions (the logic stays the
# same even as minifier output changes variable names).
def discover_retry_cap_var(data: bytes) -> bytes | None:
"""Discover the retry cap variable name from the 'clamped to' message.
The code always contains:
`CLAUDE_CODE_MAX_RETRIES=${e} clamped to ${VARNAME}`
where VARNAME=15 is the cap we want to raise.
"""
for m in re.finditer(rb'CLAUDE_CODE_MAX_RETRIES=\$\{', data):
ctx = data[m.start():m.start() + 200]
clamped = re.search(rb'clamped to \$\{([a-zA-Z_$][a-zA-Z0-9_$]*)\}', ctx)
if clamped:
return clamped.group(1)
return None
def discover_backoff_base_var(data: bytes) -> bytes | None:
"""Discover the backoff base variable name from the retry delay formula.
The code always contains:
Math.min(VARNAME*Math.pow(2,e-1),n)
where VARNAME=500 is the base delay in ms.
"""
for m in re.finditer(rb'Math\.min\(([a-zA-Z_$][a-zA-Z0-9_$]*)\*Math\.pow\(2,e-1\)', data):
return m.group(1)
return None
def discover_rate_limit_vars(data: bytes) -> tuple[bytes | None, bytes | None, bytes | None]:
"""Discover rate-limit variable names from the rate-limit handling code.
The code always contains:
R!==null&&R<THRESHOLD_VAR — if retry interval < threshold, use it
Math.max(R??FALLBACK_VAR,MIN_VAR) — fallback and minimum delays
near the string "rate_limit".
Returns (fallback_var, min_var, threshold_var) or Nones.
"""
fallback_var = None
min_var = None
threshold_var = None
idx = data.find(b'rate_limit')
while idx != -1:
before = data[max(0, idx - 500):idx]
if b'Math.max' in before:
# Extract Math.max(R??VAR1,VAR2)
pos = before.rfind(b'Math.max(R??')
if pos >= 0:
rest = before[pos + len(b'Math.max(R??'):]
m = re.match(rb'([a-zA-Z_$][a-zA-Z0-9_$]*),([a-zA-Z_$][a-zA-Z0-9_$]*)\)', rest)
if m:
fallback_var = m.group(1)
min_var = m.group(2)
# Extract R!==null&&R<THRESHOLD
m2 = re.search(rb'R!==null&&R<([a-zA-Z_$][a-zA-Z0-9_$]*)', before)
if m2:
threshold_var = m2.group(1)
if fallback_var and min_var and threshold_var:
return fallback_var, min_var, threshold_var
idx = data.find(b'rate_limit', idx + 1)
return fallback_var, min_var, threshold_var
def main():
parser = argparse.ArgumentParser(
description="Patch Claude Code binary: remove retry cap, fix backoff to 1s interval"
)
parser.add_argument("--dry-run", action="store_true", help="Show changes without modifying the binary")
parser.add_argument("--restore", action="store_true", help="Restore the original binary from backup")
args = parser.parse_args()
binary_path = find_binary()
print(f"Found binary: {binary_path}")
print(f"Binary size: {os.path.getsize(binary_path)} bytes")
backup_path = binary_path + ".orig"
# ── Restore mode ──────────────────────────────────────────────────────────
if args.restore:
if not os.path.isfile(backup_path):
print(f"ERROR: No backup found at {backup_path}", file=sys.stderr)
sys.exit(1)
print(f"Restoring original binary from {backup_path} ...")
try:
shutil.copy2(backup_path, binary_path)
os.chmod(binary_path, 0o755)
print("Restored successfully.")
except OSError as e:
print(f"ERROR: Failed to restore: {e}", file=sys.stderr)
print("Is claude running? Stop it first.", file=sys.stderr)
sys.exit(1)
return
# ── Create backup ─────────────────────────────────────────────────────────
if not os.path.isfile(backup_path):
print(f"Creating backup at {backup_path} ...")
try:
shutil.copy2(binary_path, backup_path)
except OSError as e:
print(f"ERROR: Failed to create backup: {e}", file=sys.stderr)
print("Is claude running? Stop it first.", file=sys.stderr)
sys.exit(1)
# ── Read binary ───────────────────────────────────────────────────────────
with open(binary_path, "rb") as f:
data = bytearray(f.read())
# ── Discover minified variable names dynamically ──────────────────────────
print()
print("=== Discovering version-specific patterns ===")
retry_cap_var = discover_retry_cap_var(data)
if retry_cap_var:
print(f" Retry cap variable: {retry_cap_var.decode()}")
else:
print(" WARN: Could not discover retry cap variable", file=sys.stderr)
backoff_base_var = discover_backoff_base_var(data)
if backoff_base_var:
print(f" Backoff base variable: {backoff_base_var.decode()}")
else:
print(" WARN: Could not discover backoff base variable", file=sys.stderr)
rl_fallback_var, rl_min_var, rl_threshold_var = discover_rate_limit_vars(data)
if rl_fallback_var:
print(f" Rate-limit fallback variable: {rl_fallback_var.decode()}")
if rl_min_var:
print(f" Rate-limit minimum variable: {rl_min_var.decode()}")
if rl_threshold_var:
print(f" Rate-limit threshold variable: {rl_threshold_var.decode()}")
if not rl_fallback_var or not rl_min_var or not rl_threshold_var:
print(" WARN: Could not discover all rate-limit variables", file=sys.stderr)
# ── Save hint offset for Math.pow patch before backoff base is overwritten ──
backoff_base_hint_offset = None
if backoff_base_var:
search = backoff_base_var + b"=500"
hits = find_all(data, search)
if hits:
backoff_base_hint_offset = hits[0]
print(f" Saved backoff base hint offset: {backoff_base_hint_offset}")
# ── Apply patches ─────────────────────────────────────────────────────────
stats = {"applied": 0, "failed": 0}
# ── Patch 1: Remove 15-retry cap ──────────────────────────────────────────
print()
print("=== Patch 1: Remove 15-retry cap ===")
if retry_cap_var:
search = retry_cap_var + b"=15"
replace = retry_cap_var + b"=99"
data = apply_byte_patch(data, f"Raise retry cap from 15 to 99 ({retry_cap_var.decode()})", search, replace, stats)
else:
print(" SKIP: Retry cap variable not discovered", file=sys.stderr)
stats["failed"] += 1
# ── Patch 2a: Change backoff base from 500ms to 1000ms ────────────────────
print()
print("=== Patch 2: Replace exponential backoff with fixed 1s interval ===")
if backoff_base_var:
search = backoff_base_var + b"=500"
replace = backoff_base_var + b"=1e3"
data = apply_byte_patch(data, f"Change backoff base from 500ms to 1000ms ({backoff_base_var.decode()})", search, replace, stats)
else:
print(" SKIP: Backoff base variable not discovered", file=sys.stderr)
stats["failed"] += 1
# ── Patch 2b: Disable exponential growth ──────────────────────────────────
# Math.pow(2,e-1) → Math.pow(1,e-1) (pow(1,n) always = 1)
# Use the saved hint offset from the backoff base variable to find the
# correct occurrence (there may be multiple Math.pow(2,e-1) in the binary)
data = apply_byte_patch(
data,
"Change pow base 2→1 (disables exponential growth)",
b"Math.pow(2,e-1)",
b"Math.pow(1,e-1)",
stats,
hint_offset=backoff_base_hint_offset,
max_dist=100000,
)
# ── Patch 3: Patch Anthropic SDK built-in retry backoff ───────────────────
# 0.5*Math.pow(2,o) → 1.0*Math.pow(1,o)
# This is in the SDK code, not minified app code, so the pattern is stable
print()
print("=== Patch 3: Patch Anthropic SDK built-in retry backoff ===")
data = apply_byte_patch(
data,
"Change SDK backoff from 0.5*2^o to 1.0*1^o (fixed ~1s delay)",
b"0.5*Math.pow(2,o)",
b"1.0*Math.pow(1,o)",
stats,
)
# ── Patch 4: Lower rate-limit fallback delays ─────────────────────────────
print()
print("=== Patch 4: Lower rate-limit fallback delays ===")
if rl_fallback_var:
# Fallback: 1800000ms (30min) → 0010000ms (10s)
search = rl_fallback_var + b"=1800000"
replace = rl_fallback_var + b"=0010000"
data = apply_byte_patch(data, f"Lower rate-limit fallback from 30min to 10s ({rl_fallback_var.decode()})", search, replace, stats)
else:
print(" SKIP: Rate-limit fallback variable not discovered", file=sys.stderr)
stats["failed"] += 1
if rl_min_var:
# Minimum: 600000ms (10min) → 001000ms (1s)
search = rl_min_var + b"=600000"
replace = rl_min_var + b"=001000"
data = apply_byte_patch(data, f"Lower rate-limit minimum from 10min to 1s ({rl_min_var.decode()})", search, replace, stats)
else:
print(" SKIP: Rate-limit minimum variable not discovered", file=sys.stderr)
stats["failed"] += 1
if rl_threshold_var:
# Threshold: 20000ms (20s) → 99999ms (100s)
search = rl_threshold_var + b"=20000"
replace = rl_threshold_var + b"=99999"
data = apply_byte_patch(data, f"Raise rate-limit env-var threshold from 20s to 100s ({rl_threshold_var.decode()})", search, replace, stats)
else:
print(" SKIP: Rate-limit threshold variable not discovered", file=sys.stderr)
stats["failed"] += 1
# ── Summary ───────────────────────────────────────────────────────────────
print()
print("═══════════════════════════════════════════════════════════")
print(f" Patches applied: {stats['applied']}")
print(f" Patches skipped: {stats['failed']}")
print("═══════════════════════════════════════════════════════════")
if args.dry_run:
print()
print("DRY RUN — no changes were made.")
print("Run without --dry-run to apply patches.")
return
# ── Write patched binary ──────────────────────────────────────────────────
# Use a temp file + os.rename() to avoid "Text file busy" (ETXTBSY) when
# the binary is currently running. On Linux, rename() is atomic and
# replaces the inode — the running process keeps its old mapping, while
# new invocations use the patched binary.
if stats["applied"] > 0:
import tempfile
binary_dir = os.path.dirname(binary_path)
try:
fd, tmp_path = tempfile.mkstemp(dir=binary_dir, suffix=".tmp")
try:
os.write(fd, data)
os.close(fd)
os.chmod(tmp_path, 0o755)
os.rename(tmp_path, binary_path)
except Exception:
os.close(fd) if not getattr(fd, 'closed', True) else None
try:
os.unlink(tmp_path)
except OSError:
pass
raise
print()
print("Patches applied successfully!")
print("(Running claude sessions still use the old binary; new sessions will use the patched one.)")
except OSError as e:
print(f"\nERROR: Failed to write patched binary: {e}", file=sys.stderr)
print("Is claude running? Stop it first, then re-run this script.", file=sys.stderr)
sys.exit(1)
print()
print("To restore the original binary:")
print(f" sudo python3 {sys.argv[0]} --restore")
print()
print("Set these environment variables before running claude:")
print(" export CLAUDE_CODE_MAX_RETRIES=100 # Max retry attempts (no longer capped at 15)")
print(" export CLAUDE_CODE_RETRY_INTERVAL_MS=1 # 1s delay for rate-limit retries")
print()
print("Retry behavior after patching:")
print(" ┌─────────────────────────┬──────────────────────────────────┐")
print(" │ Setting │ Behavior │")
print(" ├─────────────────────────┼──────────────────────────────────┤")
print(" │ Max retries │ CLAUDE_CODE_MAX_RETRIES (≤99) │")
print(" │ General retry delay │ Fixed ~1 second │")
print(" │ Rate-limit retry delay │ Fixed ~1 second │")
print(" │ SDK-level retry delay │ Fixed ~0.75-1 second │")
print(" └─────────────────────────┴──────────────────────────────────┘")
print()
print("NOTE: After updating Claude Code (npm update), re-run this script.")
if __name__ == "__main__":
main()
最新回复 (5)
-
Zeus Jie 06-24 23:221楼强上 any 是吧 有点心动了 想试试
-
双色辉光 06-24 23:322楼谢谢佬的分享,看起来很不错诶。
但是不知道一直重试会不会对公益站造成困扰,导致封号,有点担心啊。 -
Thanatos 06-24 23:363楼不排除这个可能性,所以风险自负了
-
DaChui 06-25 15:354楼让MIMO改了下:
我的是V2.1.191,windows系统
可以先自己用–dry-run测试好再跑

修改后的代码
#!/usr/bin/env python3
"""
patch-retry.py — Patch Claude Code binary to:
1. Remove the 15-retry cap on CLAUDE_CODE_MAX_RETRIES
2. Replace exponential backoff with fixed 1s interval
3. Patch the Anthropic SDK's built-in retry backoff
4. Lower rate-limit fallback delays
Tested on: Claude Code v2.1.191
Usage:
sudo python3 patch-retry.py [--dry-run] [--restore]
Options:
--dry-run Show what would be changed without modifying the binary
--restore Restore the original binary from backup
Environment variables (after patching):
CLAUDE_CODE_MAX_RETRIES=100 — Max retry attempts (no longer capped at 15)
CLAUDE_CODE_RETRY_INTERVAL_MS=1 — Fixed 1s delay for rate-limit retries
Version-agnostic: This script dynamically discovers minified variable names
by searching for code structure patterns (e.g. "clamped to ${VAR}" near
"CLAUDE_CODE_MAX_RETRIES") rather than hardcoding variable names. This
allows it to work across versions where minification produces different names.
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
def find_binary() -> str:
"""Find the Claude Code binary path."""
# Windows: use 'where' command
if sys.platform == "win32":
try:
result = subprocess.run(
["where", "claude"], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
for line in result.stdout.strip().splitlines():
path = line.strip()
# Try to resolve .cmd wrapper to real binary
if path.endswith(".cmd"):
try:
with open(path, "r") as f:
content = f.read()
import re as _re
# Windows .cmd files: "%~dp0\node_modules\..."
m = _re.search(r'"(%~dp0[\\/]node_modules[^"]+)"', content)
if m:
real = m.group(1).replace("%~dp0", os.path.dirname(path))
if os.path.isfile(real):
return real
except Exception:
pass
# Check if it's a shell script wrapper (small file, not a real binary)
if os.path.isfile(path):
size = os.path.getsize(path)
if size < 10000: # Likely a wrapper script, not the real binary
# Try to find real binary in node_modules
npm_dir = os.path.join(os.path.dirname(path), "node_modules", "@anthropic-ai")
if os.path.isdir(npm_dir):
for pkg in os.listdir(npm_dir):
exe = os.path.join(npm_dir, pkg, "bin", "claude.exe")
if os.path.isfile(exe) and os.path.getsize(exe) > 100000:
return exe
else:
return path
except Exception:
pass
# Fallback: check common Windows npm global path
npm_global = os.path.join(os.environ.get("APPDATA", ""), "npm")
candidates = [
os.path.join(npm_global, "node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"),
]
for c in candidates:
if os.path.isfile(c) and os.path.getsize(c) > 100000:
return c
# Unix: try 'which' first
try:
result = subprocess.run(
["which", "claude"], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
path = os.path.realpath(result.stdout.strip())
if os.path.isfile(path):
return path
except Exception:
pass
# Fallback: look in npm global packages
try:
result = subprocess.run(
["npm", "root", "-g"], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
npm_root = result.stdout.strip()
candidates = [
os.path.join(npm_root, "@anthropic-ai/claude-code/bin/claude.exe"),
os.path.join(npm_root, "@anthropic-ai/claude-code-linux-x64/claude"),
os.path.join(npm_root, "@anthropic-ai/claude-code-linux-arm64/claude"),
os.path.join(npm_root, "@anthropic-ai/claude-code-darwin-arm64/claude"),
os.path.join(npm_root, "@anthropic-ai/claude-code-darwin-x64/claude"),
]
for c in candidates:
if os.path.isfile(c):
return os.path.realpath(c)
except Exception:
pass
print("ERROR: Could not find Claude Code binary. Is it installed?", file=sys.stderr)
print("Try: npm install -g @anthropic-ai/claude-code", file=sys.stderr)
sys.exit(1)
def find_all(data: bytes, pattern: bytes) -> list[int]:
"""Find all offsets of a byte pattern in data."""
offsets = []
start = 0
while True:
idx = data.find(pattern, start)
if idx == -1:
break
offsets.append(idx)
start = idx + 1
return offsets
def find_nearest(data: bytes, pattern: bytes, ref_offset: int, max_dist: int) -> int | None:
"""Find the offset of pattern closest to ref_offset, within max_dist."""
offsets = find_all(data, pattern)
best = None
best_dist = max_dist
for off in offsets:
dist = abs(off - ref_offset)
if dist < best_dist:
best_dist = dist
best = off
return best
def apply_byte_patch(data: bytearray, desc: str, search: bytes, replace: bytes,
stats: dict, hint_offset: int | None = None,
max_dist: int = 0) -> bytearray:
"""Apply a single search→replace byte patch. Returns modified data.
If hint_offset is provided, finds the search pattern nearest to that offset
within max_dist. Otherwise, uses the first occurrence.
"""
if len(search) != len(replace):
print(f" SKIP: {desc} — byte length mismatch ({len(search)} vs {len(replace)})", file=sys.stderr)
stats["failed"] += 1
return data
if hint_offset is not None:
offset = find_nearest(data, search, hint_offset, max_dist)
if offset is None:
print(f" WARN: Could not find pattern near hint for: {desc}", file=sys.stderr)
stats["failed"] += 1
return data
else:
offsets = find_all(data, search)
if not offsets:
print(f" WARN: Could not find pattern for: {desc}", file=sys.stderr)
stats["failed"] += 1
return data
offset = offsets[0]
# Verify the bytes at the offset match
actual = data[offset:offset + len(search)]
if actual != search:
print(f" SKIP: {desc} — byte mismatch at offset {offset}", file=sys.stderr)
print(f" Expected: {search.hex()}", file=sys.stderr)
print(f" Actual: {actual.hex()}", file=sys.stderr)
stats["failed"] += 1
return data
print(f" ✓ {desc} @ offset {offset}")
data[offset:offset + len(replace)] = replace
stats["applied"] += 1
return data
# ─── Dynamic pattern discovery ────────────────────────────────────────────────
# These functions discover minified variable names by searching for code
# structure patterns that are stable across versions (the logic stays the
# same even as minifier output changes variable names).
def discover_retry_cap_var(data: bytes) -> bytes | None:
"""Discover the retry cap variable name from the 'clamped to' message.
The code always contains:
`CLAUDE_CODE_MAX_RETRIES=${e} clamped to ${VARNAME}`
where VARNAME=15 is the cap we want to raise.
"""
for m in re.finditer(rb'CLAUDE_CODE_MAX_RETRIES=\$\{', data):
ctx = data[m.start():m.start() + 200]
clamped = re.search(rb'clamped to \$\{([a-zA-Z_$][a-zA-Z0-9_$]*)\}', ctx)
if clamped:
return clamped.group(1)
return None
def discover_backoff_base_var(data: bytes) -> bytes | None:
"""Discover the backoff base variable name from the retry delay formula.
The code always contains:
Math.min(VARNAME*Math.pow(2,e-1),n)
where VARNAME=500 is the base delay in ms.
"""
for m in re.finditer(rb'Math\.min\(([a-zA-Z_$][a-zA-Z0-9_$]*)\*Math\.pow\(2,e-1\)', data):
return m.group(1)
return None
def discover_rate_limit_vars(data: bytes) -> tuple[bytes | None, bytes | None, bytes | None]:
"""Discover rate-limit variable names from the rate-limit handling code.
Searches for patterns near "rate_limit" or "retry_after" strings.
Returns (max_delay_var, base_delay_var, threshold_var) or Nones.
"""
max_delay_var = None
base_delay_var = None
threshold_var = None
# Search near "rate_limit" and "retry_after"
for marker in [b'rate_limit', b'retry_after']:
idx = data.find(marker)
while idx != -1:
ctx = data[max(0, idx - 1000):idx + 1000]
# Pattern: Math.min(G7(l,x,VAR1),VAR2) - new version
# Extract VAR1 (base delay) from inside G7 call
# Extract VAR2 (max delay) from outer Math.min
m = re.search(rb'Math\.min\(G7\([a-zA-Z_$][a-zA-Z0-9_$]*,[a-zA-Z_$][a-zA-Z0-9_$]*,([a-zA-Z_$][a-zA-Z0-9_$]*)\),([a-zA-Z_$][a-zA-Z0-9_$]*)\)', ctx)
if m:
base_delay_var = m.group(1)
max_delay_var = m.group(2)
# Pattern: I>VAR or delay>VAR - threshold
for m2 in re.finditer(rb'[Ii]>([a-zA-Z_$][a-zA-Z0-9_$]*)', ctx):
var = m2.group(1)
# Verify it's a numeric constant assignment in the whole binary
pattern = var + rb'=\d+'
if re.search(pattern, data):
threshold_var = var
break
if max_delay_var and base_delay_var and threshold_var:
return max_delay_var, base_delay_var, threshold_var
idx = data.find(marker, idx + 1)
return max_delay_var, base_delay_var, threshold_var
def main():
parser = argparse.ArgumentParser(
description="Patch Claude Code binary: remove retry cap, fix backoff to 1s interval"
)
parser.add_argument("--dry-run", action="store_true", help="Show changes without modifying the binary")
parser.add_argument("--restore", action="store_true", help="Restore the original binary from backup")
args = parser.parse_args()
binary_path = find_binary()
print(f"Found binary: {binary_path}")
print(f"Binary size: {os.path.getsize(binary_path)} bytes")
backup_path = binary_path + ".orig"
# ── Restore mode ──────────────────────────────────────────────────────────
if args.restore:
if not os.path.isfile(backup_path):
print(f"ERROR: No backup found at {backup_path}", file=sys.stderr)
sys.exit(1)
print(f"Restoring original binary from {backup_path} ...")
try:
shutil.copy2(backup_path, binary_path)
os.chmod(binary_path, 0o755)
print("Restored successfully.")
except OSError as e:
print(f"ERROR: Failed to restore: {e}", file=sys.stderr)
print("Is claude running? Stop it first.", file=sys.stderr)
sys.exit(1)
return
# ── Create backup ─────────────────────────────────────────────────────────
if not os.path.isfile(backup_path):
print(f"Creating backup at {backup_path} ...")
try:
shutil.copy2(binary_path, backup_path)
except OSError as e:
print(f"ERROR: Failed to create backup: {e}", file=sys.stderr)
print("Is claude running? Stop it first.", file=sys.stderr)
sys.exit(1)
# ── Read binary ───────────────────────────────────────────────────────────
with open(binary_path, "rb") as f:
data = bytearray(f.read())
# ── Discover minified variable names dynamically ──────────────────────────
print()
print("=== Discovering version-specific patterns ===")
retry_cap_var = discover_retry_cap_var(data)
if retry_cap_var:
print(f" Retry cap variable: {retry_cap_var.decode()}")
else:
print(" WARN: Could not discover retry cap variable", file=sys.stderr)
backoff_base_var = discover_backoff_base_var(data)
if backoff_base_var:
print(f" Backoff base variable: {backoff_base_var.decode()}")
else:
print(" WARN: Could not discover backoff base variable", file=sys.stderr)
rl_fallback_var, rl_min_var, rl_threshold_var = discover_rate_limit_vars(data)
if rl_fallback_var:
print(f" Rate-limit fallback variable: {rl_fallback_var.decode()}")
if rl_min_var:
print(f" Rate-limit minimum variable: {rl_min_var.decode()}")
if rl_threshold_var:
print(f" Rate-limit threshold variable: {rl_threshold_var.decode()}")
if not rl_fallback_var or not rl_min_var or not rl_threshold_var:
print(" WARN: Could not discover all rate-limit variables", file=sys.stderr)
# ── Save hint offset for Math.pow patch before backoff base is overwritten ──
backoff_base_hint_offset = None
if backoff_base_var:
search = backoff_base_var + b"=500"
hits = find_all(data, search)
if hits:
backoff_base_hint_offset = hits[0]
print(f" Saved backoff base hint offset: {backoff_base_hint_offset}")
# ── Apply patches ─────────────────────────────────────────────────────────
stats = {"applied": 0, "failed": 0}
# ── Patch 1: Remove 15-retry cap ──────────────────────────────────────────
print()
print("=== Patch 1: Remove 15-retry cap ===")
if retry_cap_var:
search = retry_cap_var + b"=15"
replace = retry_cap_var + b"=99"
data = apply_byte_patch(data, f"Raise retry cap from 15 to 99 ({retry_cap_var.decode()})", search, replace, stats)
else:
print(" SKIP: Retry cap variable not discovered", file=sys.stderr)
stats["failed"] += 1
# ── Patch 2a: Change backoff base from 500ms to 1000ms ────────────────────
print()
print("=== Patch 2: Replace exponential backoff with fixed 1s interval ===")
if backoff_base_var:
search = backoff_base_var + b"=500"
replace = backoff_base_var + b"=1e3"
data = apply_byte_patch(data, f"Change backoff base from 500ms to 1000ms ({backoff_base_var.decode()})", search, replace, stats)
else:
print(" SKIP: Backoff base variable not discovered", file=sys.stderr)
stats["failed"] += 1
# ── Patch 2b: Disable exponential growth ──────────────────────────────────
# Math.pow(2,e-1) → Math.pow(1,e-1) (pow(1,n) always = 1)
# Use the saved hint offset from the backoff base variable to find the
# correct occurrence (there may be multiple Math.pow(2,e-1) in the binary)
data = apply_byte_patch(
data,
"Change pow base 2→1 (disables exponential growth)",
b"Math.pow(2,e-1)",
b"Math.pow(1,e-1)",
stats,
hint_offset=backoff_base_hint_offset,
max_dist=100000,
)
# ── Patch 3: Patch Anthropic SDK built-in retry backoff ───────────────────
# 0.5*Math.pow(2,o) → 1.0*Math.pow(1,o)
# This is in the SDK code, not minified app code, so the pattern is stable
print()
print("=== Patch 3: Patch Anthropic SDK built-in retry backoff ===")
data = apply_byte_patch(
data,
"Change SDK backoff from 0.5*2^o to 1.0*1^o (fixed ~1s delay)",
b"0.5*Math.pow(2,o)",
b"1.0*Math.pow(1,o)",
stats,
)
# ── Patch 4: Lower rate-limit fallback delays ─────────────────────────────
print()
print("=== Patch 4: Lower rate-limit fallback delays ===")
if rl_fallback_var:
# Max delay: 21600000ms (6h) → 01000000ms (10s) — must be same byte length
search = rl_fallback_var + b"=21600000"
replace = rl_fallback_var + b"=01000000"
data = apply_byte_patch(data, f"Lower rate-limit max delay from 6h to 10s ({rl_fallback_var.decode()})", search, replace, stats)
else:
print(" SKIP: Rate-limit max delay variable not discovered", file=sys.stderr)
stats["failed"] += 1
if rl_min_var:
# Base delay: 300000ms (5min) → 001000ms (1s)
search = rl_min_var + b"=300000"
replace = rl_min_var + b"=001000"
data = apply_byte_patch(data, f"Lower rate-limit base delay from 5min to 1s ({rl_min_var.decode()})", search, replace, stats)
else:
print(" SKIP: Rate-limit base delay variable not discovered", file=sys.stderr)
stats["failed"] += 1
if rl_threshold_var:
# Threshold: 60000ms (60s) → 30000ms (30s) — trigger rate-limit handling sooner
search = rl_threshold_var + b"=60000"
replace = rl_threshold_var + b"=30000"
data = apply_byte_patch(data, f"Lower rate-limit threshold from 60s to 30s ({rl_threshold_var.decode()})", search, replace, stats)
else:
print(" SKIP: Rate-limit threshold variable not discovered", file=sys.stderr)
stats["failed"] += 1
# ── Summary ───────────────────────────────────────────────────────────────
print()
print("═══════════════════════════════════════════════════════════")
print(f" Patches applied: {stats['applied']}")
print(f" Patches skipped: {stats['failed']}")
print("═══════════════════════════════════════════════════════════")
if args.dry_run:
print()
print("DRY RUN — no changes were made.")
print("Run without --dry-run to apply patches.")
return
# ── Write patched binary ──────────────────────────────────────────────────
# Use a temp file + os.rename() to avoid "Text file busy" (ETXTBSY) when
# the binary is currently running. On Linux, rename() is atomic and
# replaces the inode — the running process keeps its old mapping, while
# new invocations use the patched binary.
if stats["applied"] > 0:
import tempfile
binary_dir = os.path.dirname(binary_path)
try:
fd, tmp_path = tempfile.mkstemp(dir=binary_dir, suffix=".tmp")
fd_closed = False
try:
os.write(fd, data)
os.close(fd)
fd_closed = True
os.chmod(tmp_path, 0o755)
os.replace(tmp_path, binary_path)
except Exception:
if not fd_closed:
os.close(fd)
try:
os.unlink(tmp_path)
except OSError:
pass
raise
print()
print("Patches applied successfully!")
print("(Running claude sessions still use the old binary; new sessions will use the patched one.)")
except OSError as e:
print(f"\nERROR: Failed to write patched binary: {e}", file=sys.stderr)
print("Is claude running? Stop it first, then re-run this script.", file=sys.stderr)
sys.exit(1)
print()
print("To restore the original binary:")
if sys.platform == "win32":
print(f" python {sys.argv[0]} --restore")
else:
print(f" sudo python3 {sys.argv[0]} --restore")
print()
print("Set these environment variables before running claude:")
if sys.platform == "win32":
print(" $env:CLAUDE_CODE_MAX_RETRIES=100 # Max retry attempts (no longer capped at 15)")
print(" $env:CLAUDE_CODE_RETRY_INTERVAL_MS=1 # 1s delay for rate-limit retries")
else:
print(" export CLAUDE_CODE_MAX_RETRIES=100 # Max retry attempts (no longer capped at 15)")
print(" export CLAUDE_CODE_RETRY_INTERVAL_MS=1 # 1s delay for rate-limit retries")
print()
print("Retry behavior after patching:")
print(" ┌─────────────────────────┬──────────────────────────────────┐")
print(" │ Setting │ Behavior │")
print(" ├─────────────────────────┼──────────────────────────────────┤")
print(" │ Max retries │ CLAUDE_CODE_MAX_RETRIES (≤99) │")
print(" │ General retry delay │ Fixed ~1 second │")
print(" │ Rate-limit retry delay │ Fixed ~1 second │")
print(" │ SDK-level retry delay │ Fixed ~0.75-1 second │")
print(" └─────────────────────────┴──────────────────────────────────┘")
print()
print("NOTE: After updating Claude Code (npm update), re-run this script.")
if __name__ == "__main__":
main()
-
晨旭 06-25 23:595楼我让deepseek-v4-flash把你代码上限改成无限次了:

无限次重试
#!/usr/bin/env python3
"""
patch-retry.py — Patch Claude Code binary to:
1. Remove the 15-retry cap on CLAUDE_CODE_MAX_RETRIES
2. Replace exponential backoff with fixed 1s interval
3. Patch the Anthropic SDK's built-in retry backoff
4. Lower rate-limit fallback delays
Tested on: Claude Code v2.1.191
Usage:
sudo python3 patch-retry.py [--dry-run] [--restore]
Options:
--dry-run Show what would be changed without modifying the binary
--restore Restore the original binary from backup
Environment variables (after patching):
CLAUDE_CODE_MAX_RETRIES=100 — Max retry attempts (no longer capped at 15)
CLAUDE_CODE_RETRY_INTERVAL_MS=1 — Fixed 1s delay for rate-limit retries
Version-agnostic: This script dynamically discovers minified variable names
by searching for code structure patterns (e.g. "clamped to ${VAR}" near
"CLAUDE_CODE_MAX_RETRIES") rather than hardcoding variable names. This
allows it to work across versions where minification produces different names.
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
def find_binary() -> str:
"""Find the Claude Code binary path."""
# Windows: use 'where' command
if sys.platform == "win32":
try:
result = subprocess.run(
["where", "claude"], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
for line in result.stdout.strip().splitlines():
path = line.strip()
# Try to resolve .cmd wrapper to real binary
if path.endswith(".cmd"):
try:
with open(path, "r") as f:
content = f.read()
import re as _re
# Windows .cmd files: "%~dp0\node_modules\..."
m = _re.search(r'"(%~dp0[\\/]node_modules[^"]+)"', content)
if m:
real = m.group(1).replace("%~dp0", os.path.dirname(path))
if os.path.isfile(real):
return real
except Exception:
pass
# Check if it's a shell script wrapper (small file, not a real binary)
if os.path.isfile(path):
size = os.path.getsize(path)
if size < 10000: # Likely a wrapper script, not the real binary
# Try to find real binary in node_modules
npm_dir = os.path.join(os.path.dirname(path), "node_modules", "@anthropic-ai")
if os.path.isdir(npm_dir):
for pkg in os.listdir(npm_dir):
exe = os.path.join(npm_dir, pkg, "bin", "claude.exe")
if os.path.isfile(exe) and os.path.getsize(exe) > 100000:
return exe
else:
return path
except Exception:
pass
# Fallback: check common Windows npm global path
npm_global = os.path.join(os.environ.get("APPDATA", ""), "npm")
candidates = [
os.path.join(npm_global, "node_modules", "@anthropic-ai", "claude-code", "bin", "claude.exe"),
]
for c in candidates:
if os.path.isfile(c) and os.path.getsize(c) > 100000:
return c
# Unix: try 'which' first
try:
result = subprocess.run(
["which", "claude"], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
path = os.path.realpath(result.stdout.strip())
if os.path.isfile(path):
return path
except Exception:
pass
# Fallback: look in npm global packages
try:
result = subprocess.run(
["npm", "root", "-g"], capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
npm_root = result.stdout.strip()
candidates = [
os.path.join(npm_root, "@anthropic-ai/claude-code/bin/claude.exe"),
os.path.join(npm_root, "@anthropic-ai/claude-code-linux-x64/claude"),
os.path.join(npm_root, "@anthropic-ai/claude-code-linux-arm64/claude"),
os.path.join(npm_root, "@anthropic-ai/claude-code-darwin-arm64/claude"),
os.path.join(npm_root, "@anthropic-ai/claude-code-darwin-x64/claude"),
]
for c in candidates:
if os.path.isfile(c):
return os.path.realpath(c)
except Exception:
pass
print("ERROR: Could not find Claude Code binary. Is it installed?", file=sys.stderr)
print("Try: npm install -g @anthropic-ai/claude-code", file=sys.stderr)
sys.exit(1)
def find_all(data: bytes, pattern: bytes) -> list[int]:
"""Find all offsets of a byte pattern in data."""
offsets = []
start = 0
while True:
idx = data.find(pattern, start)
if idx == -1:
break
offsets.append(idx)
start = idx + 1
return offsets
def find_nearest(data: bytes, pattern: bytes, ref_offset: int, max_dist: int) -> int | None:
"""Find the offset of pattern closest to ref_offset, within max_dist."""
offsets = find_all(data, pattern)
best = None
best_dist = max_dist
for off in offsets:
dist = abs(off - ref_offset)
if dist < best_dist:
best_dist = dist
best = off
return best
def apply_byte_patch(data: bytearray, desc: str, search: bytes, replace: bytes,
stats: dict, hint_offset: int | None = None,
max_dist: int = 0) -> bytearray:
"""Apply a single search→replace byte patch. Returns modified data.
If hint_offset is provided, finds the search pattern nearest to that offset
within max_dist. Otherwise, uses the first occurrence.
"""
if len(search) != len(replace):
print(f" SKIP: {desc} — byte length mismatch ({len(search)} vs {len(replace)})", file=sys.stderr)
stats["failed"] += 1
return data
if hint_offset is not None:
offset = find_nearest(data, search, hint_offset, max_dist)
if offset is None:
print(f" WARN: Could not find pattern near hint for: {desc}", file=sys.stderr)
stats["failed"] += 1
return data
else:
offsets = find_all(data, search)
if not offsets:
print(f" WARN: Could not find pattern for: {desc}", file=sys.stderr)
stats["failed"] += 1
return data
offset = offsets[0]
# Verify the bytes at the offset match
actual = data[offset:offset + len(search)]
if actual != search:
print(f" SKIP: {desc} — byte mismatch at offset {offset}", file=sys.stderr)
print(f" Expected: {search.hex()}", file=sys.stderr)
print(f" Actual: {actual.hex()}", file=sys.stderr)
stats["failed"] += 1
return data
print(f" ✓ {desc} @ offset {offset}")
data[offset:offset + len(replace)] = replace
stats["applied"] += 1
return data
# ─── Dynamic pattern discovery ────────────────────────────────────────────────
# These functions discover minified variable names by searching for code
# structure patterns that are stable across versions (the logic stays the
# same even as minifier output changes variable names).
def discover_retry_cap_var(data: bytes) -> bytes | None:
"""Discover the retry cap variable name from the 'clamped to' message.
The code always contains:
`CLAUDE_CODE_MAX_RETRIES=${e} clamped to ${VARNAME}`
where VARNAME=15 is the cap we want to raise.
"""
for m in re.finditer(rb'CLAUDE_CODE_MAX_RETRIES=\$\{', data):
ctx = data[m.start():m.start() + 200]
clamped = re.search(rb'clamped to \$\{([a-zA-Z_$][a-zA-Z0-9_$]*)\}', ctx)
if clamped:
return clamped.group(1)
return None
def discover_backoff_base_var(data: bytes) -> bytes | None:
"""Discover the backoff base variable name from the retry delay formula.
The code always contains:
Math.min(VARNAME*Math.pow(2,e-1),n)
where VARNAME=500 is the base delay in ms.
"""
for m in re.finditer(rb'Math\.min\(([a-zA-Z_$][a-zA-Z0-9_$]*)\*Math\.pow\(2,e-1\)', data):
return m.group(1)
return None
def discover_rate_limit_vars(data: bytes) -> tuple[bytes | None, bytes | None, bytes | None]:
"""Discover rate-limit variable names from the rate-limit handling code.
Searches for patterns near "rate_limit" or "retry_after" strings.
Returns (max_delay_var, base_delay_var, threshold_var) or Nones.
"""
max_delay_var = None
base_delay_var = None
threshold_var = None
# Search near "rate_limit" and "retry_after"
for marker in [b'rate_limit', b'retry_after']:
idx = data.find(marker)
while idx != -1:
ctx = data[max(0, idx - 1000):idx + 1000]
# Pattern: Math.min(G7(l,x,VAR1),VAR2) - new version
# Extract VAR1 (base delay) from inside G7 call
# Extract VAR2 (max delay) from outer Math.min
m = re.search(rb'Math\.min\(G7\([a-zA-Z_$][a-zA-Z0-9_$]*,[a-zA-Z_$][a-zA-Z0-9_$]*,([a-zA-Z_$][a-zA-Z0-9_$]*)\),([a-zA-Z_$][a-zA-Z0-9_$]*)\)', ctx)
if m:
base_delay_var = m.group(1)
max_delay_var = m.group(2)
# Pattern: I>VAR or delay>VAR - threshold
for m2 in re.finditer(rb'[Ii]>([a-zA-Z_$][a-zA-Z0-9_$]*)', ctx):
var = m2.group(1)
# Verify it's a numeric constant assignment in the whole binary
pattern = var + rb'=\d+'
if re.search(pattern, data):
threshold_var = var
break
if max_delay_var and base_delay_var and threshold_var:
return max_delay_var, base_delay_var, threshold_var
idx = data.find(marker, idx + 1)
return max_delay_var, base_delay_var, threshold_var
def main():
parser = argparse.ArgumentParser(
description="Patch Claude Code binary: remove retry cap, fix backoff to 1s interval"
)
parser.add_argument("--dry-run", action="store_true", help="Show changes without modifying the binary")
parser.add_argument("--restore", action="store_true", help="Restore the original binary from backup")
args = parser.parse_args()
binary_path = find_binary()
print(f"Found binary: {binary_path}")
print(f"Binary size: {os.path.getsize(binary_path)} bytes")
backup_path = binary_path + ".orig"
# ── Restore mode ──────────────────────────────────────────────────────────
if args.restore:
if not os.path.isfile(backup_path):
print(f"ERROR: No backup found at {backup_path}", file=sys.stderr)
sys.exit(1)
print(f"Restoring original binary from {backup_path} ...")
try:
shutil.copy2(backup_path, binary_path)
os.chmod(binary_path, 0o755)
print("Restored successfully.")
except OSError as e:
print(f"ERROR: Failed to restore: {e}", file=sys.stderr)
print("Is claude running? Stop it first.", file=sys.stderr)
sys.exit(1)
return
# ── Create backup ─────────────────────────────────────────────────────────
if not os.path.isfile(backup_path):
print(f"Creating backup at {backup_path} ...")
try:
shutil.copy2(binary_path, backup_path)
except OSError as e:
print(f"ERROR: Failed to create backup: {e}", file=sys.stderr)
print("Is claude running? Stop it first.", file=sys.stderr)
sys.exit(1)
# ── Read binary ───────────────────────────────────────────────────────────
with open(binary_path, "rb") as f:
data = bytearray(f.read())
# ── Discover minified variable names dynamically ──────────────────────────
print()
print("=== Discovering version-specific patterns ===")
retry_cap_var = discover_retry_cap_var(data)
if retry_cap_var:
print(f" Retry cap variable: {retry_cap_var.decode()}")
else:
print(" WARN: Could not discover retry cap variable", file=sys.stderr)
backoff_base_var = discover_backoff_base_var(data)
if backoff_base_var:
print(f" Backoff base variable: {backoff_base_var.decode()}")
else:
print(" WARN: Could not discover backoff base variable", file=sys.stderr)
rl_fallback_var, rl_min_var, rl_threshold_var = discover_rate_limit_vars(data)
if rl_fallback_var:
print(f" Rate-limit fallback variable: {rl_fallback_var.decode()}")
if rl_min_var:
print(f" Rate-limit minimum variable: {rl_min_var.decode()}")
if rl_threshold_var:
print(f" Rate-limit threshold variable: {rl_threshold_var.decode()}")
if not rl_fallback_var or not rl_min_var or not rl_threshold_var:
print(" WARN: Could not discover all rate-limit variables", file=sys.stderr)
# ── Save hint offset for Math.pow patch before backoff base is overwritten ──
backoff_base_hint_offset = None
if backoff_base_var:
search = backoff_base_var + b"=500"
hits = find_all(data, search)
if hits:
backoff_base_hint_offset = hits[0]
print(f" Saved backoff base hint offset: {backoff_base_hint_offset}")
# ── Apply patches ─────────────────────────────────────────────────────────
stats = {"applied": 0, "failed": 0}
# ── Patch 1: Remove 15-retry cap ──────────────────────────────────────────
print()
print("=== Patch 1: Remove 15-retry cap ===")
if retry_cap_var:
search = retry_cap_var + b"=15"
replace = retry_cap_var + b"=99"
data = apply_byte_patch(data, f"Raise retry cap from 15 to 99 ({retry_cap_var.decode()})", search, replace, stats)
else:
print(" SKIP: Retry cap variable not discovered", file=sys.stderr)
stats["failed"] += 1
# ── Patch 2a: Change backoff base from 500ms to 1000ms ────────────────────
print()
print("=== Patch 2: Replace exponential backoff with fixed 1s interval ===")
if backoff_base_var:
search = backoff_base_var + b"=500"
replace = backoff_base_var + b"=1e3"
data = apply_byte_patch(data, f"Change backoff base from 500ms to 1000ms ({backoff_base_var.decode()})", search, replace, stats)
else:
print(" SKIP: Backoff base variable not discovered", file=sys.stderr)
stats["failed"] += 1
# ── Patch 2b: Disable exponential growth ──────────────────────────────────
# Math.pow(2,e-1) → Math.pow(1,e-1) (pow(1,n) always = 1)
# Use the saved hint offset from the backoff base variable to find the
# correct occurrence (there may be multiple Math.pow(2,e-1) in the binary)
data = apply_byte_patch(
data,
"Change pow base 2→1 (disables exponential growth)",
b"Math.pow(2,e-1)",
b"Math.pow(1,e-1)",
stats,
hint_offset=backoff_base_hint_offset,
max_dist=100000,
)
# ── Patch 3: Patch Anthropic SDK built-in retry backoff ───────────────────
# 0.5*Math.pow(2,o) → 1.0*Math.pow(1,o)
# This is in the SDK code, not minified app code, so the pattern is stable
print()
print("=== Patch 3: Patch Anthropic SDK built-in retry backoff ===")
data = apply_byte_patch(
data,
"Change SDK backoff from 0.5*2^o to 1.0*1^o (fixed ~1s delay)",
b"0.5*Math.pow(2,o)",
b"1.0*Math.pow(1,o)",
stats,
)
# ── Patch 4: Lower rate-limit fallback delays ─────────────────────────────
print()
print("=== Patch 4: Lower rate-limit fallback delays ===")
if rl_fallback_var:
# Max delay: 21600000ms (6h) → 01000000ms (10s) — must be same byte length
search = rl_fallback_var + b"=21600000"
replace = rl_fallback_var + b"=01000000"
data = apply_byte_patch(data, f"Lower rate-limit max delay from 6h to 10s ({rl_fallback_var.decode()})", search, replace, stats)
else:
print(" SKIP: Rate-limit max delay variable not discovered", file=sys.stderr)
stats["failed"] += 1
if rl_min_var:
# Base delay: 300000ms (5min) → 001000ms (1s)
search = rl_min_var + b"=300000"
replace = rl_min_var + b"=001000"
data = apply_byte_patch(data, f"Lower rate-limit base delay from 5min to 1s ({rl_min_var.decode()})", search, replace, stats)
else:
print(" SKIP: Rate-limit base delay variable not discovered", file=sys.stderr)
stats["failed"] += 1
if rl_threshold_var:
# Threshold: 60000ms (60s) → 30000ms (30s) — trigger rate-limit handling sooner
search = rl_threshold_var + b"=60000"
replace = rl_threshold_var + b"=30000"
data = apply_byte_patch(data, f"Lower rate-limit threshold from 60s to 30s ({rl_threshold_var.decode()})", search, replace, stats)
else:
print(" SKIP: Rate-limit threshold variable not discovered", file=sys.stderr)
stats["failed"] += 1
# ── Summary ───────────────────────────────────────────────────────────────
print()
print("═══════════════════════════════════════════════════════════")
print(f" Patches applied: {stats['applied']}")
print(f" Patches skipped: {stats['failed']}")
print("═══════════════════════════════════════════════════════════")
if args.dry_run:
print()
print("DRY RUN — no changes were made.")
print("Run without --dry-run to apply patches.")
return
# ── Write patched binary ──────────────────────────────────────────────────
# Use a temp file + os.rename() to avoid "Text file busy" (ETXTBSY) when
# the binary is currently running. On Linux, rename() is atomic and
# replaces the inode — the running process keeps its old mapping, while
# new invocations use the patched binary.
if stats["applied"] > 0:
import tempfile
binary_dir = os.path.dirname(binary_path)
try:
fd, tmp_path = tempfile.mkstemp(dir=binary_dir, suffix=".tmp")
fd_closed = False
try:
os.write(fd, data)
os.close(fd)
fd_closed = True
os.chmod(tmp_path, 0o755)
os.replace(tmp_path, binary_path)
except Exception:
if not fd_closed:
os.close(fd)
try:
os.unlink(tmp_path)
except OSError:
pass
raise
print()
print("Patches applied successfully!")
print("(Running claude sessions still use the old binary; new sessions will use the patched one.)")
except OSError as e:
print(f"\nERROR: Failed to write patched binary: {e}", file=sys.stderr)
print("Is claude running? Stop it first, then re-run this script.", file=sys.stderr)
sys.exit(1)
print()
print("To restore the original binary:")
if sys.platform == "win32":
print(f" python {sys.argv[0]} --restore")
else:
print(f" sudo python3 {sys.argv[0]} --restore")
print()
print("Set these environment variables before running claude:")
if sys.platform == "win32":
print(" $env:CLAUDE_CODE_MAX_RETRIES=100 # Max retry attempts (no longer capped at 15)")
print(" $env:CLAUDE_CODE_RETRY_INTERVAL_MS=1 # 1s delay for rate-limit retries")
else:
print(" export CLAUDE_CODE_MAX_RETRIES=100 # Max retry attempts (no longer capped at 15)")
print(" export CLAUDE_CODE_RETRY_INTERVAL_MS=1 # 1s delay for rate-limit retries")
print()
print("Retry behavior after patching:")
print(" ┌─────────────────────────┬──────────────────────────────────┐")
print(" │ Setting │ Behavior │")
print(" ├─────────────────────────┼──────────────────────────────────┤")
print(" │ Max retries │ CLAUDE_CODE_MAX_RETRIES (≤99) │")
print(" │ General retry delay │ Fixed ~1 second │")
print(" │ Rate-limit retry delay │ Fixed ~1 second │")
print(" │ SDK-level retry delay │ Fixed ~0.75-1 second │")
print(" └─────────────────────────┴──────────────────────────────────┘")
print()
print("NOTE: After updating Claude Code (npm update), re-run this script.")
if __name__ == "__main__":
main()
* 帖子来源Linux.do
附近帖子