# 와치독 시스템 개선 — Phase 1~3 (1팀 한정승인)

## 개요
와치독 시스템 심층 평가에서 발견된 6개 핵심 이슈를 3 Phase로 수정한다.

---

## Phase 1: 핵심 버그 수정

### 1-1. 사망 원인 판정 수정 (Critical)
**파일**: `scripts/session-watchdog.sh` Line 228-237

**현재 문제**: BOT_LOG를 task/schedule_id 필터 없이 오늘 전체 로그에서 grep한다.
다른 봇의 에러 로그가 현재 task의 사망 원인으로 오분류된다.
실제 운영 데이터에서 12건 전부 `error_crash`로 오분류된 사례 확인됨.

**수정 방향**:
- grep 시 SCHEDULE_ID 또는 TASK_ID로 로그를 필터링
- BOT_LOG 경로에서 해당 봇의 cokacdir key로 로그 파일을 특정
- 필터링 결과가 없으면 `unknown` 유지 (기존 fallback)

**수정 위치**: Line 228-237 블록
```bash
# 현재 (문제)
BOT_LOG="/home/jay/.cokacdir/logs/telegram_$(date +%Y-%m-%d).jsonl"
if grep -q "context_limit\|token_limit" "$BOT_LOG" 2>/dev/null; then

# 수정 방향 — SCHEDULE_ID로 필터링
if [[ -n "$SCHEDULE_ID" && "$SCHEDULE_ID" != "null" ]]; then
    TASK_LOGS=$(grep "$SCHEDULE_ID" "$BOT_LOG" 2>/dev/null || true)
    if echo "$TASK_LOGS" | grep -q "context_limit\|token_limit"; then
        DEATH_REASON="context_limit"
    elif echo "$TASK_LOGS" | grep -q "usage_limit\|rate_limit"; then
        DEATH_REASON="usage_limit"
    elif echo "$TASK_LOGS" | grep -q "error\|crash\|panic"; then
        DEATH_REASON="error_crash"
    fi
fi
```

### 1-2. orphan-watchdog timezone 수정 (High)
**파일**: `scripts/orphan-watchdog.py` Line 46, 68

**현재 문제**: `datetime.now()` (naive)와 `datetime.fromisoformat()` (aware 가능)을 빼면 TypeError 발생

**수정**:
- Line 46: `now = datetime.now()` → `now = datetime.now(timezone.utc)` (import timezone 추가)
- Line 68: `elapsed = (now - start_time).total_seconds()` 앞에 start_time의 tzinfo 처리
  - start_time이 naive면 UTC로 가정하여 replace
  - start_time이 aware면 그대로 사용

```python
from datetime import datetime, timezone

now = datetime.now(timezone.utc)
# ...
start_time = datetime.fromisoformat(start_time_str)
if start_time.tzinfo is None:
    start_time = start_time.replace(tzinfo=timezone.utc)
elapsed = (now - start_time).total_seconds() / 60
```

### 1-3. 즉사 패턴 fast-track (High)
**파일**: `scripts/session-watchdog.sh` Line 95-106 (grace period 로직)

**현재 문제**: 봇이 시작 직후 죽어도 grace period 10분을 풀로 대기.
heartbeat 파일이 생성조차 안 됐고 프로세스도 없으면 봇은 이미 죽은 것.

**수정 방향**: grace period 내에서도 "heartbeat 미생성 + 프로세스 부재" 조건을 확인하여 fast-track 판정
```bash
# grace period 내라도 빠른 사망 감지
if [[ "$GRACE_ELAPSED" -lt "$GRACE_PERIOD" ]]; then
    # fast-track: heartbeat 파일 미생성 + 프로세스 부재 → 즉사 판정
    FAST_TRACK_THRESHOLD=180  # 3분
    if [[ "$GRACE_ELAPSED" -ge "$FAST_TRACK_THRESHOLD" ]]; then
        HB_FILE="${HEARTBEAT_DIR}/${TASK_ID}.heartbeat"
        if [[ ! -f "$HB_FILE" ]]; then
            # 프로세스 존재 여부 빠른 체크
            FAST_PID=""
            if [[ -n "$SCHEDULE_ID" && "$SCHEDULE_ID" != "null" ]]; then
                FAST_PROMPT=$(grep -rl "$SCHEDULE_ID" /home/jay/.cokacdir/system_prompt_* 2>/dev/null | head -1)
                if [[ -n "$FAST_PROMPT" ]]; then
                    FAST_HASH=$(basename "$FAST_PROMPT" | sed 's/system_prompt_\(.*\)_.*/\1/')
                    FAST_PID=$(ps -eo pid,args --no-headers -ww | grep "system_prompt_${FAST_HASH}" | grep -v grep | awk '{print $1}' | head -1) || true
                fi
            fi
            if [[ -z "$FAST_PID" ]]; then
                log "${TASK_ID}: 즉사 감지 (${GRACE_ELAPSED}s, heartbeat 미생성, 프로세스 없음) → fast-track"
                # grace period 스킵, 아래 판정 로직으로 진입
            else
                log "${TASK_ID}: 시작 ${GRACE_ELAPSED}초 전 (유예 기간) → 스킵"
                continue
            fi
        else
            log "${TASK_ID}: 시작 ${GRACE_ELAPSED}초 전 (유예 기간) → 스킵"
            continue
        fi
    else
        log "${TASK_ID}: 시작 ${GRACE_ELAPSED}초 전 (유예 기간 ${GRACE_PERIOD}초) → 스킵"
        continue
    fi
fi
```

---

## Phase 2: 로직 일원화

### 2-1. activity-watcher stuck bot 로직 제거
**파일**: `scripts/activity-watcher.py` Line 89-126

**현재 문제**: `check_and_recover_stuck_bots()` 함수가 bot-status-watchdog.py와 중복.
activity-watcher는 프로세스 체크 없이 30분 타임아웃만으로 idle 전환 → 작업 중 봇을 끊을 위험.

**수정**:
1. `activity-watcher.py`에서 `check_and_recover_stuck_bots()` 함수 삭제
2. 메인 루프의 워치독 체크 블록 (Line 300-307) 삭제
3. `last_watchdog_check` 변수 및 `WATCHDOG_CHECK_INTERVAL`, `TIMEOUT_MINUTES` 상수 삭제
4. activity-watcher는 순수하게 "processing → idle 전환 감지 + 알림 전송"만 담당

### 2-2. .done 상태 머신 문서화
**파일**: 신규 `memory/specs/done-state-machine.md`

**내용**:
```
## .done 파일 상태 전이

1. `.done` — 팀장 봇이 작업 완료 시 생성 (team_prompts.py)
2. `.done.notified` — activity-watcher가 텔레그램 알림 전송 후 생성
3. `.done.acked` — 아누가 보고서 확인 후 .done에서 rename
4. `.done.clear` — 아누가 제이회장님께 보고 완료 후 .acked에서 rename (히스토리 보존)

### 각 컴포넌트별 체크 대상
- session-watchdog.sh: `.done`, `.done.clear` 체크 (재위임 방지)
- activity-watcher.py: `.done` + 5개 processed 확장자 체크 (중복 알림 방지)
- 아누(CLAUDE.md): `.done` 발견 → 보고 → `.done.acked` rename
```

---

## Phase 3: 설정값 중앙화

### 3-1. constants.json에 와치독 설정 추가
**파일**: `config/constants.json`

기존 `session_monitoring` 섹션 외에 `watchdog` 섹션 추가:
```json
{
  "session_monitoring": { ... },
  "watchdog": {
    "stale_threshold_seconds": 600,
    "grace_period_seconds": 600,
    "fast_track_threshold_seconds": 180,
    "bot_timeout_minutes": 30,
    "zombie_threshold_minutes": 60,
    "backoff_duration_seconds": 1800,
    "rapid_refail_window_seconds": 180,
    "max_redelegations_per_cycle": 3,
    "max_retry_default": 2,
    "poll_interval_seconds": 3,
    "daemon_interval_seconds": 300
  }
}
```

### 3-2. 각 파일에서 constants.json 로드
**수정 대상 6개 파일**:
1. `scripts/session-watchdog.sh` — jq로 constants.json 읽기
2. `scripts/bot-status-watchdog.py` — json.load
3. `scripts/orphan-watchdog.py` — json.load
4. `scripts/activity-watcher.py` — json.load (Phase 2에서 축소된 설정만)
5. `utils/session_resilience.py` — 이미 기본값 사용 중, constants.json 연동
6. `scripts/session_watchdog.py` — session_resilience에 위임하므로 간접 적용

각 파일에서 하드코딩된 상수를 constants.json에서 로드하도록 변경.
로드 실패 시 기존 하드코딩 값을 fallback으로 유지 (session_monitor.py 패턴 따라가기).

---

## 검증 시나리오

### Phase 1 검증
1. **사망 원인 판정**: watchdog-deaths.jsonl에 `error_crash` 외 다른 reason이 정확히 분류되는지 — 테스트: 다른 봇의 error 로그가 있는 상태에서 heartbeat_stale로 죽은 task가 `heartbeat_stale`로 기록되면 성공
2. **timezone**: `orphan-watchdog.py`에 `+09:00` 포함된 start_time을 넣어도 TypeError 없이 정상 동작하면 성공
3. **즉사 fast-track**: heartbeat 파일 없고 프로세스 없는 task가 3분 후 감지되면 성공 (기존 10분 대비 7분 단축)

### Phase 2 검증
4. **activity-watcher 순수화**: activity-watcher 실행 중 30분 초과 processing 봇이 idle로 전환되지 않으면 성공 (bot-status-watchdog만 전환해야 함)
5. **문서**: `memory/specs/done-state-machine.md` 파일이 생성되고 4가지 상태가 명시되면 성공

### Phase 3 검증
6. **설정 중앙화**: `config/constants.json`의 `watchdog.stale_threshold_seconds`를 300으로 변경했을 때, session-watchdog.sh가 5분 임계값으로 동작하면 성공
7. **fallback**: constants.json을 삭제해도 기존 하드코딩 값으로 정상 동작하면 성공

### 기존 테스트 회귀
8. `bash scripts/tests/test_session_watchdog.sh` — 전체 PASS
9. `pytest tests/test_session_watchdog.py tests/test_bot_watchdog.py tests/test_orphan_watchdog.py` — 전체 PASS
