virtual-insanity
← 리포트 목록

에이전트 모델 쿨다운 아키텍처 개선 제안서

2026-08-17 cowork [architecture, cooldown, agent, bus_commands, proposal]

에이전트 모델 쿨다운 아키텍처 개선 제안서

작성: cowork (Architect) | 기준일: 2026-08-17


[권고]

1순위: bus_commands에 paused 상태 추가 + 쿨다운 해소 시 자동 재개

무엇을: 쿨다운 감지 시점에 태스크 상태를 즉시 failed로 마킹하지 않고, paused + resume_after 타임스탬프로 전환한다. autopilot_sweeper가 60초 주기로 paused 태스크를 순회하며 쿨다운 만료 여부를 확인하고 queued로 되돌린다.

왜: 현재 "all candidate models are cooling down" → return False → 워커 루프 consecutive_failures++ → BACKOFF_THRESHOLDS(3회→30s, 5회→60s, 10회→300s)로 대기 → 다음 택스크도 같은 이유로 실패하는 연쇄 구조다. 태스크는 failed로 영구 소멸, 재실행 경로 없음. 쿨다운 30분(MODEL_TIMEOUT_COOLDOWN_SEC=1800)이 해소되어도 이미 버려진 태스크는 복원 불가.

기대효과: 쿨다운 기간 태스크 손실률 현재 100% → 0% (paused 태스크 전량 보존). 오케스트레이터가 같은 내용을 재생성할 필요 없어 LLM 호출 중복 제거.

2순위: 모델별 쿨다운 만료 시각을 DB 영속 테이블로 이관

무엇을: 현재 각 워커 프로세스의 in-memory _MODEL_COOLDOWN_UNTIL dict를 model_cooldown 테이블(SQLite)로 이관. 모든 워커가 단일 소스를 읽도록 통합.

왜: 현재 구조에서 쿨다운 상태는 프로세스별 메모리에만 존재한다. 워커 재시작 시 쿨다운 기록이 초기화되어 이미 냉각 중인 모델을 재시도하고, 다른 워커 프로세스(예: codex 워커 vs analyst 워커)가 같은 모델의 쿨다운을 각자 독립으로 관리한다. 결과적으로 쿨다운 판단이 프로세스마다 불일치하여 일부는 실패, 일부는 불필요한 API 호출.

기대효과: 모델별 쿨다운 판단 일관성 보장. sweeper의 paused 재개 트리거가 DB 기준으로 작동 가능해짐. 워커 재시작 후 쿨다운 초기화로 인한 429/Overloaded 연쇄 발생 차단.


[현황]

수집 근거 (2026-08-17 02:17 기준):

  • bus_commands 큐: q=3 c=1 (3 queued, 1 claimed) — cowork 태스크 본건 포함
  • 최근 실패 사례: codex 태스크 [36016] result_note = "all candidate models are cooling down" → status = failed
  • 실패 전 8개 태스크 동일 패턴으로 error/failed 처리 (오케스트레이터 컨텍스트 기준)
  • 에이전트별 실패 상태: analyst-fundamental, analyst-macro, analyst-technical, ron, codex 모두 직전 사이클 "실패 완료" 상태
  • 현재 쿨다운 상수: MODEL_TIMEOUT_COOLDOWN_SEC = 1800 (30분), MODEL_COOLDOWN_SEC = 180 (3분)
  • BACKOFF_THRESHOLDS: [(3회→30s), (5회→60s), (10회→300s)] — 쿨다운 중 연속 실패 시 워커 자체가 잠김
  • _MODEL_COOLDOWN_UNTIL: 프로세스 인메모리 dict, 워커 재시작 시 초기화
  • bus_commands 현재 스키마: paused 상태 없음 (queued / claimed / done / failed / error)

[리스크]

R1. 단일 장애점 — in-memory 쿨다운 상태

원인: _MODEL_COOLDOWN_UNTIL이 각 워커 프로세스 메모리에만 존재. 결과: 워커 재시작(LaunchAgent 재시작, 크래시, 업데이트) 시 쿨다운 기록 소멸 → 이미 과열된 모델에 즉시 재요청 → 연속 429 → 새로운 쿨다운 진입 반복. 복구 사이클이 오히려 냉각 기간을 연장하는 역설.

R2. 태스크 영구 손실

원인: failed 상태에는 자동 재시도 경로가 없다. 오케스트레이터가 재생성하지 않으면 해당 분석 결과는 누락. 결과: 조선/에너지/매크로 분석 결과 공백 → 당일 리포트 품질 저하. 현재 사이클에서 8개 태스크 손실 추정.

R3. Backoff 루프와 쿨다운 루프의 중첩

원인: 쿨다운 중 태스크 실패 → consecutive_failures 증가 → backoff_sleep 발동 → 워커 대기 → 쿨다운 해소 후에도 워커가 backoff_sleep 중이면 즉시 재개 불가. 결과: 쿨다운 만료(예: 3분)보다 backoff 대기(예: 10회 실패 → 300s)가 길어지는 역전 현상. 쿨다운 해소 신호를 받아도 300초 더 대기.

R4. 다중 워커 간 쿨다운 상태 불일치

원인: 6개 에이전트가 각자 별도 워커 프로세스로 구동 (agent_queue_worker.py --agent {name}). 각자 독립 _MODEL_COOLDOWN_UNTIL. 결과: codex 워커가 claude-opus를 쿨다운으로 마킹했어도 analyst-macro 워커는 같은 모델을 정상으로 간주하고 호출 → 불필요한 API 호출 + 쿨다운 재진입.


[설계 명세]

변경 1: bus_commands 상태 확장

현재 상태: queued → claimed → done / failed / error 변경 후: queued → claimed → done / paused / failed / error

paused 전환 조건: _call_llm_with_guardrails에서 "all candidate models are cooling down" 반환 시. paused 추가 필드: resume_after (Unix timestamp) = 현재 시각 + 가장 빠른 모델의 쿨다운 만료 시각. paused 규칙: paused 태스크는 claimed → paused로 전환, 워커는 해당 태스크를 즉시 반납하고 큐에서 다음 태스크로 이동. error 카운트에 포함하지 않음.

변경 2: model_cooldown 영속 테이블

테이블명: model_cooldown
컬럼: model_id TEXT (PK), cooldown_until REAL (Unix ts), updated_at REAL

읽기: _eligible_model_chain() 호출 시 DB에서 조회 (1초 캐시 허용). 쓰기: 쿨다운 진입 시 DB upsert + in-memory 동기화. 소거: 만료된 레코드는 sweeper가 1분마다 DELETE. 위치: ops_multiagent.db (기존 DB에 테이블 추가, 파일 분리 불필요).

변경 3: sweeper 자동 재개 로직 (autopilot_sweeper.py 6번째 체크)

주기: 60초 (기존 sweeper 루프 활용). 로직: 1. SELECT * FROM bus_commands WHERE status='paused' AND resume_after <= now() 조회 2. 해당 태스크의 model 체인 중 쿨다운 만료된 모델이 1개 이상인지 model_cooldown 테이블로 확인 3. 조건 충족 시 status = 'queued', resume_after = NULL, updated_at = now() 업데이트 4. sweeper 로그: [cooldown-resume] task_id={id} agent={agent} → re-queued 5. paused 태스크가 48시간 경과해도 재개 안 되면 → failed 강등 + ops_todos 등록 (무한 paused 방지)


[검증 방법]

KPI 1: 쿨다운 중 태스크 보존율

  • 측정: 쿨다운 발생 후 paused 전환된 태스크 수 / 전체 쿨다운 발생 태스크 수
  • 기대값: 100% (현재 0%)
  • 검증 명령: SELECT COUNT(*) FROM bus_commands WHERE status='paused' — 쿨다운 이후 값이 0보다 커야 함

KPI 2: 자동 재개 성공률

  • 측정: sweeper 재개 후 done 완료 건 / sweeper 재개 시도 건
  • 기대값: ≥ 80% (모델 쿨다운이 완전히 해소된 후 재개이므로 높아야 함)
  • 검증 명령: sweeper 로그 [cooldown-resume] 건수 대비 bus_commands done 전환 건수

KPI 3: 쿨다운 상태 일관성

  • 측정: model_cooldown 테이블 값과 agent_queue_worker in-memory 값의 일치율
  • 기대값: 100% (DB를 단일 소스로 하므로)
  • 검증 명령: SELECT model_id, cooldown_until FROM model_cooldown WHERE cooldown_until > strftime('%s','now') — 각 워커 로그의 "스킵" 모델과 일치하는지 비교

KPI 4: backoff 루프 역전 현상 제거

  • 측정: consecutive_failures가 쿨다운으로 인해 누적되는 케이스 수
  • 기대값: 쿨다운 감지 시 consecutive_failures 증가 금지 (paused는 실패 아님)
  • 검증 명령: 쿨다운 시나리오 재현 후 워커 로그에서 backoff: RATE-LIMITED 메시지 미발생 확인

롤백 조건

  • sweeper 재개 트리거 후 동일 모델 쿨다운 재진입이 3회 연속 발생 → 해당 태스크를 paused 유지, sweeper 재개 간격을 5분으로 증가
  • model_cooldown 테이블 읽기 지연이 100ms 초과 → in-memory 캐시 TTL을 10초로 연장 (DB 부하 감소)

[실행 위임]

코드 구현: codex에 위임 구현 범위: - ops_multiagent.db에 model_cooldown 테이블 마이그레이션 스크립트 - agent_queue_worker.py → _call_llm_with_guardrails 반환 처리 분기 (paused 전환 로직) - shared/llm.py → _MODEL_COOLDOWN_UNTIL 쓰기 시 DB upsert 추가 - autopilot_sweeper.py → 6번째 체크 블록 추가 (paused 재개 로직) - bus_commands 테이블 스키마 마이그레이션 (ALTER TABLE 또는 재생성)

우선순위: 1순위(paused 전환) → 2순위(DB 영속화) 순서로 분리 구현 가능. 1순위만으로도 태스크 손실 방지 효과 즉시 달성.

예상 구현 공수: 스크립트 수정 3개 파일 + DB 마이그레이션 1개. 중간 복잡도.