JIN.PROC // v3.1
~/ / 프로젝트 / 2026-04-29-lazyclaude-deep-6-auto-resume

[LazyClaude 심층 6] Auto-Resume — rate-limit과 context-full을 분류하고 재시도하기

exit reason classifier · exponential backoff · snapshot-hash stall detection

DATE 2026.04.29 UPDATED 2026.04.29 READ ~ 3 MIN WORDS 582

LazyClaude 심층 6편. 긴 세션이 밤사이에 끊기면 보통 거기서 끝난다. Auto-Resume은 그 끊김에 분류재시도 를 붙인다.

문제

Claude Code 세션은 다음 이유로 끊긴다:

이유 회복 가능성
rate-limit 시간이 지나면 됨 (정확한 reset time 알 수 있음)
context-full 컨텍스트 압축 + 재개 필요
auth-expired 토큰 갱신 필요
unknown 분류 불가 — 사용자 개입

각각 대응 방법이 다르다. 한 가지 백오프 전략만 적용하면 어떤 경우에는 너무 빨리 깨고, 다른 경우에는 너무 늦게 시도한다.

분류 — 첫 번째 일

세션 종료 메시지·exit code·로그 마지막 라인을 보고 분류한다.

def classify_exit(stderr_tail: str, exit_code: int) -> str:
    s = stderr_tail.lower()
    if "rate limit" in s or "429" in s or "rate_limited" in s:
        return "rate_limit"
    if "context" in s and ("full" in s or "exceed" in s):
        return "context_full"
    if "401" in s or "unauthorized" in s or "auth" in s:
        return "auth_expired"
    return "unknown"

분류는 단순한 패턴 이어도 충분하다. 정확도보다 재현 가능성 이 중요. 잘못 분류하면 다음 시도에서 알게 된다.

reset time 파싱

rate_limit 으로 분류되었으면, 정확히 언제 다시 시도할 수 있는지를 stderr에서 뽑는다:

import re, datetime as dt

def parse_reset(s: str) -> dt.datetime | None:
    m = re.search(r"retry after\s+([0-9T:\-Z\.]+)", s, re.I)
    if not m: return None
    try:
        return dt.datetime.fromisoformat(m.group(1).replace("Z", "+00:00"))
    except ValueError:
        return None

서버가 알려 준 시각이 있으면 그 시각 까지 기다린다. 백오프 공식보다 훨씬 정확.

Exponential backoff — fallback

reset time 없이 분류만 가능한 경우는 지수 백오프:

import random

def backoff(attempt: int, base=2.0, cap=300.0) -> float:
    expo = min(cap, base * (2 ** attempt))
    jitter = random.uniform(0, expo * 0.2)
    return expo + jitter

핵심:

  • 지수 — 매 시도마다 2배
  • 상한(cap) — 무한 증가 방지 (5분 정도)
  • 지터(jitter) — 동시에 여러 클라이언트가 깨어나는 쿠팡 패턴 방지

Snapshot-hash stall detection

가끔 세션이 깨지지도, 진행하지도 않는 상태 로 멈춘다. 이건 분류기로 잡히지 않는다. 진행 자체 를 봐야 한다.

해법: 출력 스트림의 해시를 일정 간격으로 찍어서, N번 연속 같은 해시 면 stall로 판정.

import hashlib

def is_stalled(history: list[str], window=5) -> bool:
    if len(history) < window: return False
    return len(set(history[-window:])) == 1

def snapshot(s: str) -> str:
    return hashlib.sha1(s.encode()).hexdigest()[:12]

stall이 감지되면 더 이상 기다리지 않고 즉시 재개 시도.

재개 — claude –resume

분류·대기·재개의 흐름:

def auto_resume_loop(session_id, max_attempts=8):
    for attempt in range(max_attempts):
        result = run_until_exit(["claude", "--resume", session_id])
        if result.exit_code == 0:
            return "ok"
        reason = classify_exit(result.stderr_tail, result.exit_code)
        if reason == "rate_limit":
            wait_until = parse_reset(result.stderr_tail) or _backoff_clock(attempt)
            sleep_until(wait_until)
        elif reason == "context_full":
            run_compaction(session_id)
        elif reason == "auth_expired":
            return "needs_user"     # human in the loop
        else:
            time.sleep(backoff(attempt))
    return "gave_up"

auth_expired사람이 풀어야 하는 일. 자동화하지 않는다.

SessionStart / Stop hook과의 연계

Claude Code의 hook 시스템은 세션 라이프사이클의 진입/이탈에 코드를 끼울 수 있다. Auto-Resume은 두 지점을 활용:

  • Stop hook: 세션 종료 시점에 자체 종료 인지 이상 종료 인지 기록
  • SessionStart hook: 재개된 세션이 시작할 때, 직전 컨텍스트의 핵심 을 다시 주입 (handoff 노트가 있다면 그것을 로드)

이걸로 재개된 세션연속된 세션처럼 보이게 만든다.

UI — 패널과 배지

사용자가 이게 자동 재개되고 있는지 모르면 안 된다. LazyClaude는 두 위치에 표시:

  • 세션 detail 패널: 시도 횟수, 다음 시도 시각, 분류 결과
  • 세션 list의 🔄 AR 배지: Auto-Resume이 켜진 세션 표시

자동화는 보이지 않는 자동화보이는 자동화 의 차이가 크다. 후자가 신뢰를 만든다.

일반화 — 재시도 시스템의 4가지 책임

이 패턴은 LazyClaude 바깥에서도 그대로 쓸 수 있다:

  1. Classifier: 왜 실패했는가
  2. Wait policy: 분류별로 다른 대기
  3. Stall detector: 진행 자체가 멈췄는지 별도로 감시
  4. Human-in-the-loop: 사람만 풀 수 있는 실패 를 명시적으로 분리

한 줄 요약

모든 실패에 같은 백오프를 쓰지 마라. 분류대기 전략 을 정한다.


다음 편: LazyClaude 심층 7 — Multi-provider 워크플로우 이전 편: LazyClaude 심층 5 — Hyper Agent