# WATCHDOG_FALSE_ALERT_FIX — stalled 오탐 + 알림 스팸 제거

> **회장 지시 2026-07-20.** 오탐이 반복돼 실제 좀비와 구분이 안 된다.

## base / branch
- 저장소 **`/home/jay/workspace`** (ANU control-plane. ⚠️ InsuRo 아님)
- **신규 브랜치** `task/<ALLOCATED_TASK_ID>-watchdog-<TEAM>` 을 현재 `main` 에서 분기

## expected_files
```yaml
allowed_resources:
  paths: ["scripts/session-watchdog.sh", "tests/test_watchdog_noise_elimination.py"]
  new_file_limit: 1
  commands: ["git","python3","pytest","bash","shellcheck"]
  merge_policy: "none"
```
⚠️ `memory/**` 수정 금지(운영 데이터). `dispatch/**` 수정 금지.

## ★ ANU 진단 (실측 근거)

### 결함 A — grace period 가 **등록 시각** 기준이라 spawn 지연을 못 버틴다
`scripts/session-watchdog.sh:315-326` 이 `task-timers.json` 의 **`start_time`**(= dispatch 등록 시각)부터 `GRACE_PERIOD=600s` 를 센다.
그런데 **봇 실제 spawn 은 등록보다 한참 뒤**일 수 있다.

**실측 (task-2806)**
```
등록(start_time) 12:04:30
grace 만료        12:14:30   (600s)
봇 세션 dir 생성  12:19      ← 실제 기동
→ 12:14:30~12:19 = "등록됐지만 봇 미기동" 구간에서 STALLED 오판
```
그 시점 신호: heartbeat 파일 없음(`hb_age=-1`) · PID 없음 · 마커 없음 · events 없음
→ **전부 "죽음" 신호로 읽히지만 실제로는 "아직 안 태어남"** 이다.
※ 지연 원인 추정: 직전 task 취소(12:04:29) 직후라 봇이 정리 중이었음. **정상 동작 범위**다.

### 결함 B — 알림 **반복 억제(cooldown/dedup) 부재**
동일 task 가 **2분마다 무한 반복** 알림된다.
**실측 (task-2802)**: 7회 연속, `hb_age` 734→855→975→1096→1217→1337→1458s.
코드에 cooldown·dedup 없음(`.notified` 는 done 파일 전용).
→ **진짜 좀비 1건이 알림 수십 건**이 되어, 다른 알림을 묻어버린다.

### 참고 — heartbeat 부재 처리
`:401` `heartbeat 파일 없음` → `HEARTBEAT_ALIVE=false` 로 **그대로 stalled 판정에 흘러간다.**
"파일이 아직 없음"(신생)과 "파일이 오래됨"(정지)이 **구분되지 않는다.**

## 수정 요구

### 1. spawn 기준 유예 (결함 A)
- **등록 시각이 아니라 실제 spawn 을 기준**으로 판단하도록 보완하라.
- spawn 판정 근거로 쓸 수 있는 것(실사해서 확정): 봇 세션 디렉터리 `/home/jay/.cokacdir/workspace/<ID>/.task_id` 에 해당 task_id 가 적힌 디렉터리 존재 여부, heartbeat 파일 최초 생성 등.
- **아직 spawn 안 된 task 는 `stalled` 이 아니라 별도 상태**(예: `spawn-pending`)로 다루고 **알림하지 않거나 훨씬 긴 임계**를 적용하라.
- ⚠️ 단, **영원히 안 뜨는 유령**(실제 발생 사례 있음: task-2798 은 1시간 38분 산출물 0)은 **반드시 잡아야 한다.** spawn-pending 에도 **상한**을 두고 초과 시 알릴 것.
- 상한값은 **실측 spawn 지연 분포를 근거로** 정하고 보고에 근거를 남겨라(관측 사례: 18s·34s·35s·39s·45s 정상 / 15분 지연 1건 / 미기동 1건).

### 2. 알림 cooldown·dedup (결함 B)
- 동일 task 에 대해 **재알림 최소 간격**을 두어라(값은 근거와 함께 결정).
- 또는 **상태 변화 시에만** 재알림(예: 첫 감지 → escalate 전환 → 해소). 방식은 구현자가 정하되 **테스트로 고정**.
- **알림 억제 상태를 파일로 남길 것**(재시작해도 스팸이 재발하지 않게).

### 3. heartbeat 부재 vs 노후 구분
`heartbeat 파일 없음` 을 `heartbeat 오래됨` 과 **동일 취급하지 마라.** 최소한 로그·알림 본문에서 구분되게 하고, 판정 경로도 분리하라.

## ★ 반드시 지킬 것
- **진짜 좀비를 놓치면 안 된다.** 오탐을 줄이려다 **미탐(false negative)을 만들면 실패**다.
- 실증 케이스 2종이 **둘 다** 올바르게 처리돼야 한다:
  - **오탐이어야 하는 것**: 등록 후 spawn 지연 중(task-2806, 12:04 등록 → 12:19 기동) → **알림 없음**
  - **진짜 좀비**: 커밋 있고 세션 죽고 `.done` 없음(task-2802: 00:43 마지막 커밋, 이후 프로세스 0·미커밋 0) → **알림 있음** (단 반복 스팸 없이)

## 검증
- 기존 `tests/test_watchdog_noise_elimination.py` **전부 유지**(감소 0) + 신규 케이스 추가:
  - spawn-pending → 알림 0
  - spawn-pending 상한 초과(유령) → 알림 1
  - 진짜 좀비 → 알림 1, **반복 호출해도 추가 알림 0**(cooldown)
  - heartbeat 부재 vs 노후 구분
- `python3 -m pytest tests/test_watchdog_noise_elimination.py -q` PASS
- `bash -n scripts/session-watchdog.sh` 문법 검사 PASS (가능하면 `shellcheck`)
- **ANU 재실행 대상**

## ★ 절대 제약
- **PR 생성 0 · main merge 0.** 커밋 **정확히 1개**
- **금지**: 테스트 skip · assertion 완화 · **알림 자체를 끄기**(그건 해결이 아니다) · `memory/**` 운영데이터 수정 · 임계값을 근거 없이 크게 올려 미탐 유발
- 운영 중인 watchdog 을 **중단시키지 말 것**(브랜치 커밋만, 배포는 별도 승인)

## 봇 안전 프로토콜
- Edit·commit·finish 직전마다 **cancel 마커 재확인**
- **`.done` 자동마커는 완료 증거가 아니다** — ANU 독립검증이 권위. 커밋·보고서 반드시 남길 것

## 보고
- `/home/jay/workspace/memory/reports/watchdog-false-alert-fix.md` (⚠️ 이 저장소 안이므로 **git stage 하지 말 것**)
- diff / 결함 A·B 각 조치 근거 / **임계값 결정 근거(실측 분포)** / 오탐·미탐 2종 케이스 검증 원문 / 테스트 카운트 전후 / MATCH·GAP
- 봇 종료 전 ANU callback(envelope만·UTF-8 ≤3900B, collector=ANU key c119085addb0f8b7)

## goal_assertions (auto-generated)
- `python3 -m pytest tests/test_watchdog_noise_elimination.py -q`
