virtual-insanity
← 리포트 목록

discovery_digest + market_indicator 중복 발송 방지 수정

2026-04-24 discovery [hermes, telegram, dedup, discovery_digest, market_indicator]

discovery_digest + market_indicator 중복 발송 방지 수정

결론

  • 수정 완료:
  • /Users/ron/.hermes/workspace/scripts/pipeline/discovery_digest.py
  • /Users/ron/.hermes/workspace/scripts/pipeline/market_indicator_tracker.py
  • /Users/ron/.hermes/workspace/scripts/shared/telegram.py — 검증 중 발견된 os import 누락 1줄 보정
  • /Users/ron/.hermes/cron/jobs.json — 내일 새벽 모니터 job 추가
  • 백업:
  • discovery_digest.py.bak-dedup-20260424_105240
  • market_indicator_tracker.py.bak-dedup-20260424_105240
  • telegram.py.bak-os-import-20260424_105319
  • jobs.json.bak-dedup-monitor-20260424_105601

근본 원인 한 줄

  • discovery_digest.py: URL이 url이 아니라 _url에만 있는 항목을 notified_urls에 저장하지 못했고, 병렬 실행 시 state 저장 전 동일 batch가 동시에 발송될 수 있었다.
  • market_indicator_tracker.py: 활성 크론 중복은 없었고, 코드 내부에서 send_anomaly_dm()을 무조건 1회 호출한 뒤 anomalies가 있으면 같은 함수를 다시 호출해 같은 시장지표를 2회 발송했다.

1. discovery_digest.py 분석 및 수정

확인

  • 활성 Hermes job: ocRESTORE-intelligence-cluster, 매일 01:00 KST.
  • OpenClaw intelligence_cluster는 disabled.
  • sector trace에서 같은 호출부가 초 단위로 반복됨:
  • 2026-04-23 01:55:36/37/44 caller=discovery_digest.py:258
  • 2026-04-24 01:53:58/58/59 caller=discovery_digest.py:258

원인

기존 필터:

new_discoveries = [d for d in all_discoveries if d.get("url") not in notified_urls]
batch_urls = [d.get("url") for d in batch if d.get("url")]

하지만 extract_url() 결과는 _url에 들어가는 케이스가 있고, 이 경우 발송 후에도 notified_urls에 기록되지 않았다. 실제 4/24 ranto28 블로그 항목이 이 케이스라 dry-run에서 계속 “would be sent”로 남았다.

수정

  • discovery_identity() 추가: url → _url → file → _title → title 순서로 canonical identity 생성.
  • batch_fingerprint() + last_sent_fingerprint 추가.
  • discovery_digest_state.lock 파일락 추가: 병렬 실행 시 state load/filter/send/save를 한 번에 직렬화.
  • 이미 4/24 01:53에 발송된 현재 항목은 state 보정:
  • state_repaired_at: 2026-04-24T10:54:30+09:00
  • state_repair_reason 기록.

2. market_indicator_tracker.py 분석 및 수정

크론 확인

Hermes live jobs.json에서 market_indicator_tracker.py 활성 job은 1개뿐이다.

ocRESTORE-market-indicator-tracker
schedule: 5 7,9,11,13,15,17 * * 1-5
next_run_at: 2026-04-24T11:05:00+09:00

OpenClaw legacy job은 disabled:

intelligence-market-indicator-tracker enabled=false

원인

기존 notify block:

send_anomaly_dm(...)
if anomalies:
    dm_sent = send_anomaly_dm(...)

send_anomaly_dm() 자체가 market 섹터 리포트를 보내는 함수라, anomalies가 있는 날은 같은 리포트가 2회 발송됐다. sector trace의 07:05:35 / 07:05:36 2연속 발송과 일치한다.

수정

  • send_anomaly_dm() 호출을 1회로 축소.
  • .dm_sent_today.lock 파일락 추가.
  • .dm_sent_today.jsonreport_fingerprint, sent_at 저장.
  • dedup 조건을 date == today 또는 report_fingerprint 동일로 강화.

3. 검증 결과

syntax

python3 -m py_compile shared/telegram.py discovery_digest.py market_indicator_tracker.py
OK

discovery_digest 실제 실행

No new discoveries (all already notified)
sector_trace discovery_digest before=21 after=21

=> 이미 보낸 4/24 항목은 재발송되지 않음.

discovery_digest send-once harness

임시 filtered/state + monkeypatch send_sector로 2회 연속 실행:

Sent 1 discoveries
No new discoveries (all already notified)
send_calls= 1

=> 같은 batch는 1회만 발송.

market_indicator_tracker notify 재실행

Market indicator notification already sent, skipping
sector_trace market_indicator before=6 after=6
{"status": "ok", "dm_sent": false, "charts_sent": 0, ...}

=> 오늘 이미 발송된 시장지표는 재실행해도 추가 발송 없음.

4. 내일 새벽 모니터 트리거

Hermes job 추가:

id: dedup-monitor-discovery-market-20260425
enabled: true
schedule: 10 2 25 4 * Asia/Seoul
next_run_at: 2026-04-25T02:10:00+09:00
output: /Users/ron/knowledge-agent/400-reports/260425_dedup_monitor.log

역할: 4/25 02:10에 sector_trace.log에서 discovery_digest.pymarket_indicator_tracker.py 최근 발송 흔적을 스냅샷으로 남긴다.

5. 남은 리스크

  • send_sector는 trace를 dedupe 검사 전에 남기므로, sector_trace 한 줄 수와 실제 Telegram 발송 수가 1:1은 아니다. 실제 중복 여부는 이번처럼 state와 send 호출 경로를 같이 봐야 한다.
  • send_document() dedup 미적용은 이번 범위 밖이지만, 이전 감사에서 별도 리스크로 확인됨.

6. 자체평가

  • 정확성: 4.7/5 — 로그/코드/크론 직접 확인 후 원인별 수정.
  • 완성도: 4.5/5 — 두 파이프라인 모두 중복 방지 추가, 모니터 job 설정.
  • 검증: 4.5/5 — py_compile, 실제 재실행 skip, send-once harness 통과.
  • 최소 변경: 4.2/5 — 검증 차단 요인으로 shared/telegram.py import 1줄 추가가 범위 밖에 가까웠지만 필수 런타임 보정.
  • 종합: 4.5/5

DONE