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:22
    1楼

    强上 any 是吧 有点心动了 想试试

  • 双色辉光 06-24 23:32
    2楼

    谢谢佬的分享,看起来很不错诶。

    但是不知道一直重试会不会对公益站造成困扰,导致封号,有点担心啊。

  • Thanatos 06-24 23:36
    3楼

    不排除这个可能性,所以风险自负了

  • DaChui 06-25 15:35
    4楼

    让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:59
    5楼

    我让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
返回