에이전트 모델 쿨다운 아키텍처 개선 제안서
에이전트 모델 쿨다운 아키텍처 개선 제안서
작성: cowork | 일자: 2026-08-17 | 상태: 검토 대기
[권고]
1순위: bus_commands에 paused 상태 도입 + paused_until 컬럼 추가
쿨다운 감지 시 태스크를 즉시 failed 처리하는 대신 paused 상태로 보류하고, 만료 시각을 DB에 기록한다.
워커 루프(또는 sweeper)가 60초 주기로 paused → queued 복귀를 체크하여 자동 재개한다.
기대효과: 쿨다운 사유 태스크 손실 23건 → 0건. 중복 재시도([R1]/[R2]) 생성 제거.
2순위: model_cooldown_registry 테이블 신설 (인메모리 → DB 영속화)
현재 _MODEL_COOLDOWN_UNTIL 딕셔너리는 워커 프로세스 재시작 시 초기화된다.
DB 테이블로 만료 시각을 영속화하면 재시작 후에도 쿨다운 상태가 보존되고,
오케스트레이터가 모델별 가용 여부를 실시간으로 조회할 수 있다.
기대효과: 쿨다운 잔여 시간 가시성 확보, 워커 재시작 후 동일 모델 즉시 재호출 방지.
[현황]
쿨다운 발생 경로 (실측)
모델 타임아웃 2회 연속
→ _MODEL_TIMEOUT_STREAKS[model_id] >= 2
→ _MODEL_COOLDOWN_UNTIL[model_id] = now + 1800초 (30분)
→ _eligible_model_chain() 에서 해당 모델 제외
→ 전체 모델 체인 소진 시: note = "all candidate models are cooling down"
→ return False, note, ""
→ 호출부에서 즉시 bus_commands.status = "failed" 전환
실제 피해 규모 (bus_commands DB 기준)
| 구분 | 건수 |
|---|---|
| 현재 활성 failed (쿨다운 사유) | 3건 |
| archived failed (쿨다운 사유) | 20건 |
| 누적 손실 합계 | 23건 |
확인된 쿨다운 failed 사례:
- #36016 codex — [R1] 8개 실패 태스크 공통 원인 진단 → all candidate models are cooling down
- #36018 analyst-fundamental — [R1] 조선 사이클 선가·수주 현황 재수집 → 동일
- #36017 analyst-macro — [R1] 8/18-22 주요 매크로 이벤트 정리 → 동일
현재 스키마의 구조적 한계
- 인메모리 쿨다운:
_MODEL_COOLDOWN_UNTIL= Python 딕셔너리, 프로세스 재시작 시 소실 - paused 상태 부재:
bus_commands.status허용값:queued / claimed / done / failed— 일시 보류 상태 없음 - 재큐 이중 차단:
_INFRA_FAIL_PATTERNS에 "cooldown" 포함 → 오케스트레이터 재큐 차단FAILED_DUPLICATE_WINDOW_SEC = 7200초(2시간)→ 쿨다운 30분 해소 후에도 같은 제목 중복 방지로 재생성 차단- 가시성 없음: 어느 모델이 언제까지 쿨다운인지 대시보드/로그 외 확인 불가
[리스크]
R1. 쿨다운 중 태스크 축적 → 쿨다운 해소 후 burst 발생
- paused 태스크가 동시에 queued로 복귀하면 모델 호출이 집중되어 재쿨다운 유발 가능
- 완화: paused → queued 복귀를 에이전트당 1건씩 staggered 방식으로 처리
R2. paused 상태 태스크 영구 방치
- 쿨다운 DB 레코드가 삭제/오염되면 paused 태스크가 queued로 복귀하지 못함
- 완화: sweeper가 paused_until 초과 후 1시간 이상 미복귀 시 forced queued 전환 + ops_todos 등록
R3. 단일 장애점: model_cooldown_registry 테이블
- DB 락 경쟁 시 워커 다수가 동시에 쿨다운 레지스트리를 읽고 쓸 때 일관성 문제
- 완화: WAL 모드 사용(이미 ops_multiagent.db 적용 추정), 쿨다운 등록은 INSERT OR REPLACE로 원자성 보장
R4. 기존 재시도 로직([R1]/[R2]) 과의 충돌
_INFRA_FAIL_PATTERNS에서 "cooldown" 제거 시 오케스트레이터가 쿨다운 태스크를 다시 재큐할 수 있음- 완화: paused 상태 도입 후에는 오케스트레이터 재큐 로직이 paused 건을 건너뛰도록 필터 추가
[설계 명세]
1. bus_commands 스키마 변경
현재:
status: queued | claimed | done | failed
변경:
status: queued | claimed | paused | done | failed
paused_until: TEXT (ISO8601, NULL이면 무기한)
paused_reason: TEXT (사유: model_cooldown / credit_guard / manual)
마이그레이션:
ALTER TABLE bus_commands ADD COLUMN paused_until TEXT;
ALTER TABLE bus_commands ADD COLUMN paused_reason TEXT;
2. model_cooldown_registry 테이블 신설
CREATE TABLE IF NOT EXISTS model_cooldown_registry (
model_id TEXT PRIMARY KEY,
agent TEXT, -- 쿨다운을 발생시킨 에이전트
cooldown_until REAL, -- Unix timestamp
timeout_streak INTEGER DEFAULT 0,
triggered_by TEXT, -- timeout | rate_limit | manual
created_at TEXT,
updated_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_mcr_until ON model_cooldown_registry(cooldown_until);
3. agent_queue_worker.py 변경 포인트 (코드 작성은 codex 위임)
변경 위치 A: _call_llm_with_guardrails() 반환 처리
현재: return False, "all candidate models are cooling down", ""
변경: return False, "all candidate models are cooling down", "PAUSE_UNTIL:{만료_timestamp}"
→ 호출부에서 반환값 세 번째 필드를 파싱하여 paused_until 계산
변경 위치 B: _register_model_cooldown() 신규 함수
- _MODEL_COOLDOWN_UNTIL 딕셔너리 갱신과 동시에 DB model_cooldown_registry INSERT OR REPLACE
변경 위치 C: _eligible_model_chain() 수정
- 인메모리 딕셔너리 우선 조회, miss 시 DB fallback 조회 (워커 재시작 복원)
변경 위치 D: 워커 메인 루프 또는 sweeper
- 60초 주기로 SELECT id FROM bus_commands WHERE status='paused' AND paused_until <= unixepoch() 조회
- 해당 건을 status='queued', paused_until=NULL 전환 (에이전트당 1건 제한 burst 방지)
변경 위치 E: orchestrator.py _INFRA_FAIL_PATTERNS
- "cooldown" 항목 제거 (paused 상태로 대체되므로 error follow-up 불필요)
- _INFRA_NOISE에서도 "cooldown" 제거
4. 자동 재개 트리거 플로우
[워커 루프 / autopilot_sweeper.py 60s 주기]
1. SELECT bus_commands WHERE status='paused' AND paused_until <= now()
2. 결과 있음 → 에이전트별 1건씩 status='queued' 복귀
3. model_cooldown_registry에서 해당 모델 쿨다운 만료 여부 확인
4. 만료 확인 → _MODEL_COOLDOWN_UNTIL 딕셔너리 갱신 (워커 인메모리 동기화)
5. ops_agent_events에 "paused_task_resumed" 이벤트 기록
[검증 방법]
KPI 기준
| 지표 | 현재 | 목표 |
|---|---|---|
| 쿨다운 사유 failed 건수 (30일) | 23건 | 0건 |
| 쿨다운 후 자동 재개율 | 0% | >= 95% |
| 재시도([R1]) 생성 중 쿨다운 사유 비율 | 확인 불가 | 실시간 대시보드 가시화 |
| 쿨다운 해소 후 재개 지연 | N/A (수동 재생성) | <= 120초 (2 sweeper 사이클) |
검증 명령 (구현 후)
# 1. DB 스키마 확인
sqlite3 ~/.hermes/data/ops_multiagent.db ".schema bus_commands" | grep paused
sqlite3 ~/.hermes/data/ops_multiagent.db ".schema model_cooldown_registry"
# 2. paused 태스크 현황
sqlite3 ~/.hermes/data/ops_multiagent.db \
"SELECT id, target_agent, title, paused_until, paused_reason FROM bus_commands WHERE status='paused'"
# 3. 모델 쿨다운 레지스트리 현황
sqlite3 ~/.hermes/data/ops_multiagent.db \
"SELECT model_id, datetime(cooldown_until,'unixepoch','localtime') as expires, timeout_streak FROM model_cooldown_registry ORDER BY cooldown_until DESC"
# 4. 자동 재개 이벤트 확인
sqlite3 ~/.hermes/data/ops_multiagent.db \
"SELECT agent, event_data, created_at FROM ops_agent_events WHERE event_type='paused_task_resumed' ORDER BY created_at DESC LIMIT 10"
롤백 조건
아래 중 하나 발생 시 paused 상태 도입 롤백 (paused → failed 즉시 전환으로 복귀): - paused 태스크가 24시간 이상 queued 복귀 없이 방치된 건이 5건 이상 - 쿨다운 해소 후 burst 재호출로 동일 모델이 1시간 내 3회 이상 재쿨다운
[실행 위임]
| 단계 | 담당 | 내용 |
|---|---|---|
| DB 마이그레이션 스크립트 작성 | codex | ALTER TABLE + CREATE TABLE DDL + 안전 실행 스크립트 |
| agent_queue_worker.py 변경 A~D | codex | 위 변경 포인트 4곳 구현 |
| orchestrator.py 변경 E | codex | _INFRA_FAIL_PATTERNS / _INFRA_NOISE 수정 |
| autopilot_sweeper.py 재개 루프 추가 | codex | paused → queued 복귀 체크 루프 |
| 검증 및 ops_todos 등록 | ron | 구현 완료 후 위 검증 명령 실행 |
| 해리 최종 승인 | 해리 | DB 스키마 변경(컬럼 추가)은 운영 DB 변경이므로 확인 후 실행 |
제안서 생성: cowork | 근거 데이터: bus_commands DB 직접 조회 (23건 손실 실측) + agent_queue_worker.py L395-925 코드 분석