# task-561.1 — .done 알림 시스템 재설계: 3사이클 재검토 보고서

**작성**: 헤르메스 (dev1-team 팀장)
**작성일**: 2026-03-14
**미팅 기록**: `/home/jay/workspace/memory/meetings/2026-03-14-done-notification-direct-telegram.md`

---

## 최종 판정: 수정 찬성 (Conditional Approve)

미팅 합의안의 방향성(직접 Telegram API 호출 + 전송 후 선점)은 **정확하고 실현 가능**하다.
단, 아래 3가지 수정사항을 반영해야 구현이 완전하다:

1. **done-watcher.sh의 숨은 버그** — 미팅에서 명시적으로 언급되지 않았으나 실제 로그에서 확인됨
2. **토큰 결정** — ANU_BOT_TOKEN 신규 추가가 아닌 기존 GROUP_CHAT_BOT_TOKEN 재사용 검토 필요
3. **cokacdir 완전 제거가 아닌 선택적 유지** — 체인 중간 Phase의 아누 세션 깨우기는 별도 검토

---

## 사이클 1: 아키텍처 검증

### 1.1 현재 코드 — cokacdir --cron의 실제 동작

`cokacdir --cron`은 **새 Claude AI 세션을 스케줄링**하는 명령이다. Telegram 메시지를 직접 보내는 것이 아니다.

동작 흐름:
- `cokacdir --cron "프롬프트" --at "1m"` → 1분 후 새 Claude 세션 생성
- 새 세션이 시작되면 해당 프롬프트를 실행
- 세션이 Telegram 채팅에 응답하는 형태로 "알림"이 전달됨

**문제**: 아누 세션이 비활성일 때 `cokacdir`가 새 세션을 만들어도:
- 새 세션 자체가 실패할 수 있음 (인프라 의존)
- 새 세션이 프롬프트를 제대로 처리하지 못할 수 있음
- "알림"이 아닌 "새 AI 작업"을 생성하는 것이므로 오버헤드가 큼

→ **미팅 합의 타당**: `requests.post()`로 Telegram Bot API 직접 호출이 올바른 방향

### 1.2 실현 가능성 검증

| 항목 | 상태 | 비고 |
|------|------|------|
| `requests` 모듈 | ✅ v2.32.5 설치됨 | stdlib `urllib` 도 fallback 가능 |
| Telegram Bot API | ✅ 안정 API | `POST /bot{TOKEN}/sendMessage` |
| 봇 토큰 | ⚠️ 결정 필요 | `.env.keys`에 `GROUP_CHAT_BOT_TOKEN` 존재, `ANU_BOT_TOKEN`은 없음 |
| systemd timer | ✅ 활성 | `done-watcher.timer` 30초 간격 동작 중 |
| chat_id | ✅ | `6937032012` (제이회장님 채팅) |

### 1.3 봇 토큰 현황

`.env.keys` 현재 상태:
- `GROUP_CHAT_BOT_TOKEN=8617441526:AAF3HWKr8xjRB_Tq5LI4t_Qzl9WeW8zvR6k` ← **이미 존재**
- `ANU_BOT_TOKEN` ← **존재하지 않음**

미팅 합의안은 "ANU_BOT_TOKEN 추가"라고 했지만:
- `GROUP_CHAT_BOT_TOKEN`이 chat_id `6937032012`에 메시지를 보낼 수 있다면 새 토큰 불필요
- Telegram 봇은 사용자가 `/start`를 누른 적 있으면 해당 chat_id에 메시지 전송 가능
- **사전 검증 필수**: `GROUP_CHAT_BOT_TOKEN`으로 테스트 메시지 전송 → 성공 시 이 토큰 사용, 실패 시 별도 봇 필요

### 1.4 미팅에서 놓친 점

#### (A) done-watcher.sh의 "선점 후 호출" 버그 — 실제 로그로 증명됨

`done-watcher.log` 최근 기록:
```
[2026-03-14T13:10:20+09:00] L3 감지: task-546.1 → notify 호출
[SKIP] task-546.1: 이미 처리됨 (.done.clear 존재). 아누 세션 미깨움.
```

버그 경로:
1. `done-watcher.sh`가 `.done.clear`를 **먼저** 원자적 생성 (O_EXCL)
2. `notify-completion.py` 호출
3. `notify-completion.py`는 `.done.clear` 존재 확인 → `[SKIP]` → return
4. **알림이 전혀 전송되지 않음**

미팅에서는 `notify-completion.py`의 선점-실패 버그만 논의했지만, `done-watcher.sh`에도 **동일한 구조적 결함**이 있다. L3 fallback이 사실상 작동하지 않는 상태.

#### (B) wake_anu_session()의 이중 역할

`wake_anu_session()`은 단순 알림이 아닌 **새 아누 세션을 생성하여 완료 처리를 시키는** 역할이다.
직접 Telegram API로 교체하면 "알림"은 가지만 "자동 처리"는 사라진다.

이것이 괜찮은 이유:
- `user-prompt-submit.sh` Hook(L2)이 `.done` 파일을 감지하여 아누에게 안내
- 아누가 어떤 이유로든 깨어나면 Hook이 미처리 작업을 자동 포착
- 현재 시스템은 아누를 깨우려 해도 100% 실패하므로, 수동 알림 + Hook 조합이 **현실적으로 더 신뢰성 높음**

#### (C) 더 단순한 대안

미팅 DA(Devil's Advocate)에서 "done-watcher.sh에 5줄 추가면 충분"이라는 의견이 나왔다.
재평가: **불충분**. `notify-completion.py`(L1)도 반드시 수정해야 한다.
- L1이 cokacdir에 의존하는 한 1차 시도 자체가 실패
- L3(watcher)만 수정하면 알림까지 최소 60초 지연 + L3 자체의 선점 버그도 해결해야 함
- **L1 + L3 모두 수정이 정답** (미팅 합의와 동일)

---

## 사이클 2: 엣지케이스 + 보안

### 2.1 Telegram Bot API rate limit

- 제한: 초당 30메시지/봇, 같은 채팅에 1초 1메시지 권장
- 최악 시나리오: 3팀 동시 완료 → 3건/초 → 제한 이내
- **결론: 문제 없음**

### 2.2 봇 토큰 노출 방지

현재 `.env.keys`는 `chmod 640`으로 보호. 스크립트에서 환경변수로 로딩:
```bash
source /home/jay/workspace/.env.keys
```

지켜야 할 규칙:
- 스크립트에 토큰 하드코딩 절대 금지
- `os.environ.get("GROUP_CHAT_BOT_TOKEN")` 또는 `$GROUP_CHAT_BOT_TOKEN` 사용
- 로그에 토큰 출력 금지 (requests URL에 토큰 포함되므로 URL 로깅 시 마스킹)
- git tracked 파일에 토큰 절대 불포함

### 2.3 서버 재부팅 시 미전송 .done 처리

- `.done` 파일은 디스크에 영속 → 재부팅 후에도 존재
- systemd timer `done-watcher.timer`가 부팅 시 자동 시작되면 → watcher가 감지하여 전송
- **확인 필요**: `done-watcher.timer`가 `WantedBy=multi-user.target` 등으로 부팅 시 활성화되는지

### 2.4 네트워크 단절 시 retry 로직

현재: retry 없음 (1회 시도 후 실패 시 종료)
제안: 3회 retry + 지수 백오프

```python
def send_direct_telegram(bot_token: str, chat_id: str, message: str) -> bool:
    import requests, time
    url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
    for attempt in range(3):
        try:
            resp = requests.post(url, json={
                "chat_id": chat_id,
                "text": message,
                "parse_mode": "HTML"
            }, timeout=10)
            if resp.status_code == 200:
                return True
            # 429 Too Many Requests → retry
            if resp.status_code == 429:
                retry_after = resp.json().get("parameters", {}).get("retry_after", 5)
                time.sleep(retry_after)
                continue
        except requests.RequestException:
            pass
        time.sleep(2 ** attempt)  # 1s, 2s, 4s
    return False
```

**핵심**: 전송 실패 시 `.done.clear` 생성하지 않음 → watcher(L3)가 후속 시도 가능

### 2.5 Race Condition 분석

#### 시나리오 1: L1과 L3 동시 전송 (전송 후 선점 방식)
- L1(notify-completion): 전송 성공 → `.done.clear` O_EXCL 시도
- L3(done-watcher): `.done` 발견, `.done.clear` 없음 → 전송 성공 → `.done.clear` O_EXCL 시도
- 결과: 둘 다 전송, 하나만 `.done.clear` 성공 → **중복 알림 1건**
- **허용됨** (미팅 합의: 누락 > 중복)

#### 시나리오 2: L1 전송 성공 직후 서버 크래시
- L1이 Telegram 전송 완료했으나 `.done.clear` 생성 전 크래시
- 재부팅 후 L3가 `.done` 발견 → 다시 전송 → `.done.clear` 생성
- 결과: **중복 알림 1건** → 허용됨

#### 시나리오 3: 체인 중간 Phase — dispatch 실패
- 현재 코드(notify-completion.py:193-208): dispatch 실패 시 `.done.clear` 삭제하여 재시도 허용
- 이 로직은 유지해야 함. 직접 Telegram 전환 시에도 dispatch 실패 핸들링 보존 필요

#### 시나리오 4: `.done` 파일이 watcher find와 notify 사이에 삭제됨
- 가능성 낮지만, `.done` 파일 읽기 실패 시 graceful 처리 필요
- `send_direct_telegram()`은 `.done` 파일 내용에 의존하지 않으므로 영향 없음

**새로운 race condition 없음** — "전송 후 선점"이 기존 "선점 후 전송"보다 모든 면에서 안전.

---

## 사이클 3: 구현 계획 + 테스트 설계

### 3.1 코드 변경 범위

#### 파일 1: `/home/jay/workspace/scripts/notify-completion.py`

**변경 내용:**
1. `send_direct_telegram()` 함수 추가 (위 코드 스케치 참조)
2. `wake_anu_session()` 내부 수정:
   - `cokacdir --cron` 제거
   - `send_direct_telegram()` 호출로 교체
   - 봇 토큰은 `os.environ.get("GROUP_CHAT_BOT_TOKEN")` (또는 결정된 토큰 이름)
3. `send_telegram_notification()` (L79-98) 동일하게 수정
4. **else 분기 (L217-235) 순서 변경**:
   - 현재: `.done.clear` 생성 → `wake_anu_session()`
   - 변경: `send_direct_telegram()` → 성공 시에만 `.done.clear` 생성
5. **chain 중간 Phase (L165-215) 순서 변경**:
   - 현재: `create_done_clear()` → dispatch → telegram
   - 변경: dispatch → telegram → 성공 시 `create_done_clear()`

**코드 스케치 (else 분기):**
```python
# 변경 전
try:
    fd = os.open(str(done_clear_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
    os.write(fd, ...)
    os.close(fd)
except FileExistsError:
    print(f"[SKIP] ...")
    return
wake_anu_session(args.task_id, args.chat_id, anu_key)

# 변경 후
bot_token = os.environ.get("GROUP_CHAT_BOT_TOKEN")
if not bot_token:
    print("[FATAL] GROUP_CHAT_BOT_TOKEN 환경변수 미설정", file=sys.stderr)
    sys.exit(1)

message = f"✅ {args.task_id} 완료\n보고서: /home/jay/workspace/memory/reports/{args.task_id}.md"
sent = send_direct_telegram(bot_token, args.chat_id, message)

if sent:
    # 전송 성공 후에만 선점
    try:
        fd = os.open(str(done_clear_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
        os.write(fd, json.dumps({"source": "notify-completion", "task_id": args.task_id}).encode())
        os.close(fd)
    except FileExistsError:
        pass  # 다른 프로세스가 이미 처리 — 중복 알림 허용
else:
    print(f"[WARN] Telegram 전송 실패. .done.clear 미생성 → L3 재시도 대기", file=sys.stderr)
```

#### 파일 2: `/home/jay/workspace/scripts/done-watcher.sh`

**변경 내용:**
- `.done.clear` 원자적 생성을 **알림 전송 이후로** 이동
- `notify-completion.py` 호출 대신 직접 `curl`로 Telegram API 호출 (bash 스크립트이므로)
- 또는: 수정된 `notify-completion.py`를 호출하되, `.done.clear`를 watcher가 먼저 만들지 않음

**권장 접근: Option A — done-watcher도 notify-completion.py 호출, 단 선점 제거**

```bash
# 변경 전
if python3 -c "O_EXCL .done.clear 생성" 2>/dev/null; then
    source .env.keys
    python3 notify-completion.py "$task_id"  # ← .done.clear 이미 존재 → SKIP!
fi

# 변경 후
# .done.clear 존재 확인만 (생성하지 않음)
[ -f "$clear_file" ] && continue

# 60초 미만이면 L1에게 기회 양보
file_age=$(( $(date +%s) - $(stat -c %Y "$done_file") ))
[ "$file_age" -lt 60 ] && continue

echo "[$(date -Iseconds)] L3 감지: $task_id → 직접 전송" >> "$LOG"
source /home/jay/workspace/.env.keys 2>/dev/null || true

# 직접 Telegram API 호출
RESULT=$(curl -s -w "%{http_code}" -o /dev/null \
    -X POST "https://api.telegram.org/bot${GROUP_CHAT_BOT_TOKEN}/sendMessage" \
    -H "Content-Type: application/json" \
    -d "{\"chat_id\":\"6937032012\",\"text\":\"⚠️ [L3 Fallback] ${task_id} 완료 알림 (L1 미전송 감지)\"}" 2>/dev/null)

if [ "$RESULT" = "200" ]; then
    # 전송 성공 후 선점
    python3 -c "
import os, sys
try:
    fd = os.open('$clear_file', os.O_CREAT | os.O_EXCL | os.O_WRONLY)
    os.write(fd, b'{\"source\":\"done-watcher\",\"task_id\":\"$task_id\"}')
    os.close(fd)
except FileExistsError:
    pass  # L1이 이미 처리 — OK
" 2>/dev/null
    echo "[$(date -Iseconds)] L3 전송 성공 + .done.clear 생성: $task_id" >> "$LOG"
else
    echo "[$(date -Iseconds)] L3 전송 실패 (HTTP $RESULT), 다음 주기 재시도: $task_id" >> "$LOG"
fi
```

#### 파일 3: `/home/jay/workspace/.env.keys`

**변경 내용:**
- `GROUP_CHAT_BOT_TOKEN`으로 chat_id 6937032012에 전송 가능한지 사전 검증
- 가능하면: 변경 없음 (기존 토큰 사용)
- 불가능하면: `ANU_BOT_TOKEN` 추가 또는 별도 `NOTIFY_BOT_TOKEN` 추가

### 3.2 E2E 테스트 시나리오

#### 사전 조건
- 아누(cokacdir) 세션 완전 종료 상태
- `GROUP_CHAT_BOT_TOKEN`으로 테스트 메시지 전송 가능 확인

#### 테스트 1: L1 정상 경로 (notify-completion 직접 전송)
1. 테스트용 `.done` 파일 생성: `echo '{"task_id":"test-e2e-1"}' > events/test-e2e-1.done`
2. `python3 notify-completion.py test-e2e-1` 실행
3. 검증: Telegram 메시지 수신 + `.done.clear` 생성 확인
4. 정리: `test-e2e-1.done`, `test-e2e-1.done.clear` 삭제

#### 테스트 2: L3 Fallback 경로 (done-watcher 감지)
1. 테스트용 `.done` 파일 생성 (타임스탬프 2분 전으로 설정)
2. 30초 대기 (done-watcher 주기)
3. 검증: Telegram 메시지 수신 + `.done.clear` 생성 확인
4. 정리

#### 테스트 3: 전송 실패 시 재시도
1. `.env.keys`에 잘못된 토큰 임시 설정
2. `notify-completion.py` 실행 → 실패 확인
3. `.done.clear`가 생성되지 않았는지 확인
4. 올바른 토큰 복원
5. done-watcher가 다음 주기에 성공적으로 전송하는지 확인

#### 테스트 4: 중복 알림 허용 확인
1. `.done` 생성 → L1 즉시 전송 → `.done.clear` 미생성 상태에서 L3도 전송
2. Telegram 메시지 2건 수신 확인 → 정상

#### 테스트 5: 체인 중간 Phase
1. 체인 소속 작업의 `.done` 생성
2. notify-completion.py 실행
3. 검증: dispatch 성공 + Telegram 알림 수신 + `.done.clear` 생성

### 3.3 Rollback 계획

1. **변경 전 백업**:
   ```bash
   cp notify-completion.py notify-completion.py.bak
   cp done-watcher.sh done-watcher.sh.bak
   ```

2. **기능 플래그** (선택사항):
   ```python
   USE_DIRECT_TELEGRAM = os.environ.get("USE_DIRECT_TELEGRAM", "1") == "1"
   ```
   `0`으로 설정하면 기존 cokacdir 방식으로 복원

3. **즉시 복원**:
   ```bash
   cp notify-completion.py.bak notify-completion.py
   cp done-watcher.sh.bak done-watcher.sh
   ```

### 3.4 6개월 유지 가능성 평가

**유지 가능**: ✅
- Telegram Bot API는 2015년부터 안정적. `sendMessage`는 breaking change 가능성 극히 낮음
- `requests` 라이브러리는 Python 생태계 표준
- 토큰 관리는 `.env.keys` 단일 파일 — 토큰 교체 시 한 곳만 수정
- 3팀 규모에서 파일 기반 `.done` 시스템은 충분 (팀 10개 이상 시 SQLite 검토)

**잠재 리스크**:
- 봇 토큰 revoke 시 즉시 장애 → `.env.keys` 업데이트 프로세스 필요
- Telegram 서버 장애 시 알림 전체 중단 → retry 로직으로 일시적 장애 대응, 장기 장애는 대안 없음 (이메일 등 보조 채널은 과도)

---

## 수정 제안 요약

미팅 합의 6개 항목에 대한 판정:

### 1. Telegram Bot API 직접 호출 → ✅ 찬성
근거: `cokacdir --cron`은 "새 AI 세션 생성"이지 "메시지 전송"이 아님. 아키텍처 불일치.

### 2. 전송 후 선점 → ✅ 찬성
근거: "선점 후 실패" 버그가 L1, L3 모두에서 확인됨. 전송 후 선점이 유일한 해법.

### 3. done-watcher 동일 적용 → ✅ 찬성 + 수정 추가
근거: 미팅에서 명시하지 않았으나, done-watcher의 자체 선점 로직도 수정 필수.
**수정**: done-watcher가 `.done.clear`를 먼저 생성하는 코드를 제거하고, 직접 전송 후 선점으로 변경.

### 4. 봇 토큰: ANU_BOT_TOKEN 추가 → ⚠️ 수정 제안
근거: `GROUP_CHAT_BOT_TOKEN`이 이미 `.env.keys`에 존재. 사전 검증 후 재사용 가능하면 신규 토큰 불필요.
**수정**: 구현 전 `GROUP_CHAT_BOT_TOKEN`으로 chat_id 6937032012에 테스트 전송. 성공 시 이 토큰 사용, 실패 시 별도 토큰 추가.

### 5. E2E 테스트 필수 → ✅ 찬성
근거: 3회 연속 실패 이력 감안, 테스트 없는 배포는 4번째 실패 가능. 위 5개 시나리오 적용.

### 6. 중복 알림 허용 → ✅ 찬성
근거: "전송 후 선점" 방식에서 L1+L3 동시 전송은 구조적으로 발생 가능. 누락보다 중복이 안전.

---

## 구현 지시서 (Dispatch 가능 수준)

### Phase 1: 토큰 검증 (5분)
```bash
# GROUP_CHAT_BOT_TOKEN으로 테스트 전송
source /home/jay/workspace/.env.keys
curl -s -X POST "https://api.telegram.org/bot${GROUP_CHAT_BOT_TOKEN}/sendMessage" \
  -H "Content-Type: application/json" \
  -d '{"chat_id":"6937032012","text":"[TEST] 알림 시스템 토큰 검증"}'
```
- 200 OK → `GROUP_CHAT_BOT_TOKEN` 사용 확정
- 403 Forbidden → 별도 봇 토큰 필요 → 제이회장님에게 에스컬레이션

### Phase 2: notify-completion.py 수정
- `send_direct_telegram()` 함수 추가 (retry 3회, 지수 백오프)
- `wake_anu_session()` → `send_direct_telegram()` 교체
- `send_telegram_notification()` → `send_direct_telegram()` 교체
- else 분기: 전송 → 선점 순서 변경
- chain 분기: dispatch → 전송 → 선점 순서 변경

### Phase 3: done-watcher.sh 수정
- `.done.clear` 선점 제거 → 존재 확인만 수행
- `notify-completion.py` 호출 제거 → `curl`로 직접 Telegram API 호출
- 전송 성공 후에만 `.done.clear` 생성

### Phase 4: E2E 테스트 (3회)
- 아누 세션 종료 상태 확인
- 테스트 1~5 실행
- 3회 연속 성공 확인

### Phase 5: Rollback 준비
- 변경 전 파일 백업
- 기능 플래그 환경변수 문서화

---

## 생성/수정 파일 목록

이 작업은 검토/설계만 수행. 코드 수정 없음.
- **생성**: `/home/jay/workspace/memory/reports/task-561.1.md` (본 보고서)

## 테스트 결과
해당 없음 (검토 작업)

## 버그 발견
- **done-watcher.sh 선점-실패 버그**: watcher가 `.done.clear`를 먼저 생성한 뒤 `notify-completion.py`를 호출하나, notify-completion.py는 `.done.clear` 존재 시 SKIP → L3 fallback이 사실상 무효화. 실제 로그(`task-546.1`)에서 확인됨.

## 비고
- 이 보고서의 코드 스케치는 구현 참고용. 실제 구현 시 팀원이 코드를 작성하고 테스트해야 함.
- 토큰 검증(Phase 1)은 구현 작업 dispatch 전에 제이회장님 확인 필요 (테스트 메시지가 Telegram에 전송됨).
