virtual-insanity
← 리포트 목록

weekly briefing 두 주차 연속 발송 버그 추적

2026-04-24 weekly [weekly-briefing, telegram, launchd, duplicate, audit]

Executive Summary

  • weekly-briefing-telegram.log에는 2건이 붙어 보이지만, 실제 발송 시각은 2026-04-15 16:49:072026-04-20 07:00:04로 서로 다른 실행이다.
  • 코드상 build_weekly_message()는 현재일 기준 today - 7일 ~ today 1개 주차만 만들고, main()_send_report_topic()을 1회만 호출한다.
  • 근본 원인은 B 코드 루프 버그도 A 중복 cron도 아니고, C 수동/초기 검증 실행 1회 + 정기 월요일 실행 1회가 같은 stdout 로그에 timestamp 없이 append되어 “한 번 실행에 2주차”처럼 보인 것이다.

1. 로그 증거

weekly 전용 stdout 로그

파일: /Users/ron/.hermes/logs/weekly-briefing-telegram.log

[briefing_wrapper] kind=weekly chars=366 meta={"done": 18, "queued": 5, "avg_kpi": "N/A", "discoveries": 0}
[SECTOR_TRACE] send_sector(report) caller=telegram_briefing_wrapper.py text='<b>📅 주간 운영·시장 리포트 · Telegram</b>\n<i>2026-04-08 ~ 2026-04-15 / source=weekly-tele'
[briefing_wrapper] send_result={"ok": true, "message_ids": [2351], "error": null, "chat_id": -1003522748967, "topic_id": 8, "transport": "telegram_api"}
[briefing_wrapper] kind=weekly chars=379 meta={"done": 18, "queued": 16, "avg_kpi": "N/A", "discoveries": 0}
[SECTOR_TRACE] send_sector(report) caller=telegram_briefing_wrapper.py text='<b>📅 주간 운영·시장 리포트 · Telegram</b>\n<i>2026-04-13 ~ 2026-04-20 / source=weekly-tele'
[briefing_wrapper] send_result={"ok": true, "message_ids": [2551], "error": null, "chat_id": -1003522748967, "topic_id": 8, "transport": "telegram_api"}

주의: 이 로그 파일은 자체 timestamp를 찍지 않는다. 그래서 두 실행이 연속 줄처럼 보인다.

timestamp가 있는 sector_trace 로그

파일: /Users/ron/.hermes/logs/sector_trace.log

2026-04-15 16:49:07 [SECTOR_TRACE] send_sector(report) caller=telegram_briefing_wrapper.py text='<b>📅 주간 운영·시장 리포트 · Telegram</b>\n<i>2026-04-08 ~ 2026-04-15 / source=weekly-tele'
2026-04-20 07:00:04 [SECTOR_TRACE] send_sector(report) caller=telegram_briefing_wrapper.py text='<b>📅 주간 운영·시장 리포트 · Telegram</b>\n<i>2026-04-13 ~ 2026-04-20 / source=weekly-tele'

판정:

msg_id 주차 실제 발송 시각 의미
2351 2026-04-08 ~ 2026-04-15 2026-04-15 16:49:07 KST wrapper/plist 생성 당일 수동 또는 kickstart 검증 실행
2551 2026-04-13 ~ 2026-04-20 2026-04-20 07:00:04 KST 월요일 07:00 정기 launchd 실행

2026-04-22, 2026-04-23, 2026-04-24에는 weekly briefing 발송 흔적이 없다. weekly-briefing-telegram.log의 mtime도 2026-04-20 07:00:05 +0900이다.

2. 크론 잡 매트릭스

LaunchAgent

label 경로 스케줄 상태 ProgramArguments 로그
com.openclaw.weekly-briefing-telegram /Users/ron/Library/LaunchAgents/com.openclaw.weekly-briefing-telegram.plist 월요일 07:00 (Weekday=1, Hour=7, Minute=0) loaded, runs = 2, last exit code = 0 /bin/bash /Users/ron/.hermes/workspace/scripts/pipeline/weekly_briefing_wrapper.sh /Users/ron/.hermes/logs/weekly-briefing-telegram.log
com.openclaw.weekly-report /Users/ron/Library/LaunchAgents/com.openclaw.weekly-report.plist 일요일 20:00 (Weekday=0) 별도 TTS /Users/ron/.hermes/skills/smart-home/scripts/tts_weekly.sh Telegram report topic 아님
com.openclaw.kpi-weekly /Users/ron/Library/LaunchAgents/com.openclaw.kpi-weekly.plist 월요일 07:50 별도 KPI kpi_telegram_wrapper.py --weekly ops topic 성격, weekly briefing 아님

LaunchAgent grep 결과: weekly_briefing_wrapper 또는 --kind weekly를 호출하는 plist는 com.openclaw.weekly-briefing-telegram.plist 하나뿐이다.

Hermes jobs.json

~/.hermes/cron/jobs.json에는 telegram_briefing_wrapper.py --kind weekly 또는 weekly_briefing_wrapper.sh를 호출하는 enabled job이 없다.

검색에서 걸린 weekly 계열은 다음처럼 별도 도메인이다.

id/name schedule enabled 성격 중복 여부
bond-weekly-review 금 19:00 true 채권 주간 리뷰 무관
bond-weekly-score 월 09:00 true 채권 주간 점수 무관
fundamental-*, ocTC-twitter-collector 매일 06시대 일부 true/false morning briefing prereq 태그 때문에 검색 매칭 무관

~/.openclaw/cron/jobs.jsonweekly-synthesis, morning-briefing 등은 disabled 상태다.

3. 코드 로직

파일: /Users/ron/.hermes/workspace/scripts/pipeline/telegram_briefing_wrapper.py

핵심 경로:

def build_weekly_message() -> tuple[str, dict]:
    today = _now().date()
    week_ago = today - timedelta(days=7)
    ...
    message = "\n".join([
        "<b>📅 주간 운영·시장 리포트 · Telegram</b>",
        f"<i>{week_ago.isoformat()} ~ {today.isoformat()} / source=weekly-telegram-wrapper</i>",
        ...
    ])
    return message, {...}

if args.kind == "morning":
    message, meta = build_morning_message(...)
else:
    message, meta = build_weekly_message()

result = _send_report_topic(message, parse_mode="HTML")

확인 사항:

  • build_weekly_message() 내부에 주차를 순회하는 반복문 없음.
  • main()args.kind == weekly일 때 build_weekly_message()를 1회만 호출.
  • _send_report_topic()은 Telegram message chunk만 순회한다. 현재 weekly 메시지는 366~379자로 chunk 1개라 message_id도 1개씩만 생성됐다.
  • dry-run 재현 결과도 1개 메시지만 생성:
[briefing_wrapper] kind=weekly chars=359 meta={...}
<b>📅 주간 운영·시장 리포트 · Telegram</b>
<i>2026-04-17 ~ 2026-04-24 / source=weekly-telegram-wrapper</i>
...
counts: kind=weekly 1, title 1, SECTOR_TRACE 0(dry-run)

4. 근본 원인

판정: C 수동/초기 검증 실행 + 정기 실행

후보 판정 근거
A. 중복 잡 LaunchAgent에서 weekly wrapper 호출자는 1개. Hermes jobs.json에도 weekly wrapper job 없음.
B. 코드 루프 버그 weekly 생성 함수와 main 경로 모두 1회 호출. dry-run도 메시지 1개.
C. 수동 재실행/초기 검증 첫 발송은 plist 생성 당일인 04-15 16:49. 정기 스케줄은 월요일 07:00이므로 정기 실행일 수 없음. 04-20 07:00은 정기 스케줄과 정확히 일치. launchctl print도 runs = 2.
D. 기타 ⚠️ 로그 파일에 timestamp가 없어서 두 실행이 한 실행처럼 보이는 관찰성 결함이 있다.

보조 근거:

  • com.openclaw.weekly-briefing-telegram.plistweekly_briefing_wrapper.sh 생성/수정 시각: 2026-04-15 16:38:49 +0900.
  • 첫 weekly 발송: 2026-04-15 16:49:07 +0900 — 생성 직후 검증 발송으로 보는 것이 자연스럽다.
  • 두 번째 weekly 발송: 2026-04-20 07:00:04 +0900 — plist의 StartCalendarInterval과 일치.

5. 수정 권고

이번 작업에서는 jobs.json/plist 수정은 하지 않았다. 권고만 남긴다.

권고 1 — 로그 timestamp 추가

telegram_briefing_wrapper.pyprint()에 timestamp를 붙이거나 launchd stdout wrapper에서 ts를 prefix한다.

예시 diff:

+def _log_line(message: str) -> None:
+    print(f"[{_now().strftime('%Y-%m-%d %H:%M:%S KST')}] {message}", flush=True)
...
-    print(f"[briefing_wrapper] kind={args.kind} chars={len(message)} meta={json.dumps(meta, ensure_ascii=False)}")
+    _log_line(f"[briefing_wrapper] kind={args.kind} chars={len(message)} meta={json.dumps(meta, ensure_ascii=False)}")
...
-    print(f"[briefing_wrapper] send_result={json.dumps(result, ensure_ascii=False)}")
+    _log_line(f"[briefing_wrapper] send_result={json.dumps(result, ensure_ascii=False)}")

권고 2 — weekly 중복 방지 dedupe state 추가

수동 kickstart와 정기 발송이 같은 기준일에 겹칠 때를 막으려면 ~/.hermes/state/weekly_briefing_sent.jsonweek_endmessage_id를 저장하고, --force 없이는 동일 week_end 재발송을 막는다.

예시 정책:

if kind == weekly and state.week_end == today.isoformat() and not --force:
    skip send, log duplicate guard

권고 3 — launchd 표준 로그 회전/분리

현재 weekly-briefing-telegram.log는 계속 append되어 과거 실행이 붙어 보인다. 날짜별 로그 또는 매 실행 header를 추가한다.

echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] weekly briefing start"

자체평가

  • 정확성: 5/5 — timestamp 있는 sector_trace, launchctl, plist, jobs.json, 코드 경로를 대조했다.
  • 완성도: 4.5/5 — 중복 원인 판정과 수정 권고까지 완료. 실제 수정은 요청대로 보류했다.
  • 검증: 4.5/5 — dry-run으로 1-message 경로 재현. 실제 발송 재현은 스팸 방지를 위해 하지 않았다.
  • 최소 변경: 5/5 — 보고서만 생성, jobs/plist/code 수정 없음.

종합: 4.75/5

DONE