virtual-insanity
← 리포트 목록

알림센터 발송 파이프라인 중복 체크 룰 감사

2026-04-24 pipeline [pipeline, dedup, telegram, hermes, audit]

알림센터 발송 파이프라인 중복 체크 룰 감사

  • 감사 시각: 2026-04-24 10:43:35 KST
  • 범위: /Users/ron/.hermes/workspace/scripts/pipeline/**/*.pysend_dm( / send_sector( / send_document( 호출 파일
  • 기준: “이전 발송 내용과 동일하면 스킵. memory/ 또는 DB에 마지막 발송 해시 저장 후 비교”를 파이프라인 자체 dedup으로 구현했는가
  • 주의: shared/telegram.py에는 1시간 exact-text TTL dedupe가 있으나, 이것은 폭격 방지용 transport guard라서 일/주 단위 동일 리포트 재발송 방지 규칙을 완전히 만족하지 못한다고 판정했다.

1. 결론

  • 발송 호출 파일: 107개 (_archive 제외)
  • 파이프라인 자체 발송 dedup 명확 구현: 5개
  • 위반/미흡 파이프라인: 102개
  • 소스 처리상태만 있음: 39개
  • 공통 Telegram 1시간 TTL dedupe만 의존: 54개
  • dedup 없음/직접 전송: 9개
  • send_document 호출 파일: 5개shared.telegram.send_document() 자체에는 _is_duplicate() 호출이 없어 문서 전송은 공통 dedupe도 받지 않는다.

가장 위험한 TOP 3 파일: channel_collector.py, bond_morning_quiz.py, blog_monitor.py.

2. 공통 전송부 확인

  • /Users/ron/.hermes/workspace/scripts/shared/telegram.py
  • _DEDUP_FILE = ~/.hermes/run/telegram_dedup.json
  • _DEDUP_TTL_SECONDS = 3600
  • send_dm / send_sector_dedupe_and_send() 또는 _is_duplicate() 경유
  • send_document()send_document_result()만 호출하며 _is_duplicate() 경유 없음

판정: 공통 dedupe는 “1시간 내 같은 문자열 폭격 방지”에는 유효하지만, CLAUDE 룰의 “마지막 발송 해시 저장 후 동일 리포트 스킵”과는 범위가 다르다. 그래서 각 파이프라인의 memory/DB 기반 last_sent_hash 또는 last_sent_date+hash가 필요하다.

3. 위반 사례 TOP 5

1) channel_collector.py

  • 발송 함수: send_sector
  • 활성 크론: ocRESTORE-analyst-channel-collector:45 7,13 * * 1-5; oc118-channel-collector:17 3,9,15,21 * * *; oc119-trillion-labs-collector:0 6,14,21 * * *
  • dedup 판정: 부분(소스/처리상태)
  • 빈 결과 스킵: Y
  • 등급 게이트: Y(shared/sector)
  • 위험 근거: 하루 약 8.4회 활성 스케줄, last_msg_id로 소스 신규 여부만 보며 최종 발송 본문 hash 비교 없음.
  • 최소 수정 권고: build_report() 직후/send_sector 직전(약 1015행)에 report_hash = sha256(report)를 만들고 memory/channel-collector/state.jsonlast_sent_hash 저장. 1078행의 summary 알림도 별도 hash 또는 같은 gate 사용.

2) bond_morning_quiz.py

  • 발송 함수: send_dm
  • 활성 크론: fd0119a08cc4:30 7 * * 1-5; bc3a02bc769c:0 14 * * 1-5
  • dedup 판정: N
  • 빈 결과 스킵: Y
  • 등급 게이트: N(custom/direct)
  • 위험 근거: 직접 Telegram API 함수(def send_dm) 사용. 공통 dedupe/등급 게이트 우회.
  • 최소 수정 권고: 233행/257행 send_dm(args.chat_id, ...) 직전에 ~/.hermes/workspace/memory/bond-quiz/last_sent.json 기반 date+quiz_hash gate 추가. 가능하면 custom send_dm 제거 후 shared.telegram.send_dm(level="info") 사용.

3) blog_monitor.py

  • 발송 함수: send_sector
  • 활성 크론: ocM-M019-blog-monitor:every 360m
  • dedup 판정: 부분(소스/처리상태)
  • 빈 결과 스킵: Y
  • 등급 게이트: Y(shared/sector)
  • 위험 근거: 6시간마다 실행. processed GUID는 있으나 최종 발송 텍스트 hash gate 없음.
  • 최소 수정 권고: 574행 send_sector("ideas", text) 직전에 memory/blog-monitor/last_sent.json{blog_id, guid, body_hash} 저장/비교. 현재 .processed_blogs.json은 처리 guids 기준이라 발송 본문 동일성 보장은 아님.

4) nepcon_collector.py

  • 발송 함수: send_sector
  • 활성 크론: ocJ-J000-nepcon:37 1,7,13,19 * * *
  • dedup 판정: 부분(소스/처리상태)
  • 빈 결과 스킵: Y
  • 등급 게이트: Y(shared/sector)
  • 위험 근거: 하루 4회. seen article state는 있으나 Telegram report 자체 중복 발송 gate 없음.
  • 최소 수정 권고: 676행 _send_telegram(processed) 내부에서 processed article id 목록 + report hash를 memory/nepcon/state.json.last_sent_hash로 비교 후 동일하면 skip.

5) bond_evening_concept.py

  • 발송 함수: send_dm
  • 활성 크론: 0a258438a36e:0 21 * * *
  • dedup 판정: N
  • 빈 결과 스킵: Y
  • 등급 게이트: N(custom/direct)
  • 위험 근거: 직접 Telegram API 함수 사용, 공통 dedupe/등급 게이트 우회.
  • 최소 수정 권고: 120행 전송 직전 concept_id/date/message_hashmemory/bond-evening-concept/last_sent.json에 저장/비교. custom send_dm은 shared.telegram으로 대체 권고.

4. 감사 매트릭스

파일 발송 함수 크론 주기 dedup 있음? 빈 결과 스킵? 등급 게이트?
channel_collector.py send_sector ocRESTORE-analyst-channel-collector:45 7,13 * * 1-5; oc118-channel-collector:17 3,9,15,21 * * *; oc119-trillion-labs-… 부분(소스/처리상태) Y Y(shared/sector)
bond_morning_quiz.py send_dm fd0119a08cc4:30 7 * * 1-5; bc3a02bc769c:0 14 * * 1-5 N Y N(custom/direct)
blog_monitor.py send_sector ocM-M019-blog-monitor:every 360m 부분(소스/처리상태) Y Y(shared/sector)
nepcon_collector.py send_sector ocJ-J000-nepcon:37 1,7,13,19 * * * 부분(소스/처리상태) Y Y(shared/sector)
bond_evening_concept.py send_dm 0a258438a36e:0 21 * * * N Y N(custom/direct)
oil_supply_monitor.py send_sector ocCRIT-oil-supply-monitor-global:0 6 * * ; ocRESTORE-oil-supply-monitor-afternoon:0 14 * * ; ocCRIT-oil-supply-moni… 부분(소스/처리상태) Y Y(shared/sector)
analyst_quality_tracker.py send_dm ocRESTORE-analyst-quality-tracker:10 8 * * * 공통TTL만(1h exact) Y Y(shared/sector)
goal_alignment.py send_sector oc-goal-alignment:43 12 * * * 공통TTL만(1h exact) Y Y(shared/sector)
inbox_health_auditor.py send_dm ocRESTORE-inbox-health-auditor:30 0 * * * 공통TTL만(1h exact) Y Y(shared/sector)
kr_research_collector.py send_dm kr-research-collector:15 4 * * * 공통TTL만(1h exact) Y Y(shared/sector)
moat_scorer.py send_dm ocRESTORE-moat-scorer:17 3 * * * N Y 불명
refining_tracker.py send_sector ocRESTORE-refining-tracker:40 6 * * * 공통TTL만(1h exact) Y Y(shared/sector)
sector_research.py send_sector ocRESTORE-sector-research:37 3 * * * 공통TTL만(1h exact) Y Y(shared/sector)
tanker_tracker.py send_sector ocRESTORE-tanker-tracker:35 6 * * * 공통TTL만(1h exact) Y Y(shared/sector)
vault_flow_health.py send_sector oc-vault-flow-health:13 5 * * * 공통TTL만(1h exact) Y Y(shared/sector)
bond_weekly_review.py send_dm 35dac8a246ff:0 19 * * 5 N Y N(custom/direct)
bond_weekly_score.py send_dm 1659adf98d3b:0 9 * * 1 N Y N(custom/direct)
bond_market_close.py send_dm 1d04396f15ec:40 15 * * 1-5 공통TTL만(1h exact) Y Y(shared/sector)
fnguide_snapshot.py send_dm ocRESTORE-fnguide-snapshot:0 17 * * 1-5 공통TTL만(1h exact) Y Y(shared/sector)
fundamental_collector.py send_sector ocRESTORE-vault-fundamental-bridge:0 7 * * 1-5 공통TTL만(1h exact) Y Y(shared/sector)
macro_series_collector.py send_dm macro-series-collector:50 6 * * 1-5 공통TTL만(1h exact) Y Y(shared/sector)
market_indicator_tracker.py send_sector ocRESTORE-market-indicator-tracker:5 7,9,11,13,15,17 * * 1-5 Y(발송/일자 해시·state) Y Y(shared/sector)
telegram_popular_posts.py send_sector ocRESTORE-popular-posts-collect:20 7,13 * * 1-5 부분(소스/처리상태) Y Y(shared/sector)
dart_raw_financials.py send_document - 부분(소스/처리상태) Y N(document 직접)
generate_methodology_report.py send_document - 공통TTL만(1h exact) Y N(document 직접)
generate_methodology_report_v2.py send_document - N Y N(document 직접)
shipbuilding_excel_builder.py send_document - 공통TTL만(1h exact) Y N(document 직접)
analyst_evolution_tracker.py send_sector ocRESTORE-analyst-evolution-tracker:15 8 * * * 부분(소스/처리상태) Y Y(shared/sector)
discovery_digest.py send_sector ocRESTORE-intelligence-cluster:0 1 * * * 부분(소스/처리상태) Y Y(shared/sector)
discovery_filter.py send_sector ocRESTORE-intelligence-cluster:0 1 * * * 부분(소스/처리상태) Y Y(shared/sector)
hypothesis_engine.py send_sector ocRESTORE-intelligence-cluster:0 1 * * * 부분(소스/처리상태) Y Y(shared/sector)
hypothesis_lifecycle.py send_dm ocRESTORE-hypothesis-lifecycle:45 3 * * * 부분(소스/처리상태) Y Y(shared/sector)
twitter_collector.py send_sector ocTC-twitter-collector:20 6 * * * 부분(소스/처리상태) Y Y(shared/sector)
vault_analyst_feedback.py send_sector 41c2736f0527:50 7 * * * 부분(소스/처리상태) Y Y(shared/sector)
vault_architect.py send_sector oc-vault-architect:27 4 * * * 부분(소스/처리상태) Y Y(shared/sector)
note_atomizer.py send_sector ocRESTORE-note-atomizer:37 2,14 * * * Y(발송/일자 해시·state) Y Y(shared/sector)
daily_report.py send_sector ocAK-AK000-bond-daily-dry-run:0 9 * * 2-6; ocAK-AK100-bond-evening-report:37 22 * * 1-5 Y(발송/일자 해시·state) Y Y(shared/sector)
copper_market_collector.py send_dm ocRESTORE-copper-market-collector:30 7 * * 5 공통TTL만(1h exact) Y Y(shared/sector)
keyword_tuner.py send_sector ocRESTORE-keyword-tuner:30 4 * * 1,4 부분(소스/처리상태) Y Y(shared/sector)
awesome_scout.py send_dm aasf-scout-1b68bd38:7 9 * * 1 부분(소스/처리상태) Y 불명
urea_price_tracker.py send_sector ocRESTORE-urea-price-tracker:0 9 * * 1 Y(발송/일자 해시·state) Y Y(shared/sector)
agent_community_report.py send_dm/send_document - 공통TTL만(1h exact) Y Y(shared/sector)
analyst_calibration.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
analyst_prediction_backfill.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
blueprint_updater.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
china_macro_collector.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
choi_report_collector.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
claude_practice_monitor.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
commodity_spike_analyzer.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
company_insight_tracker.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
context_review_loop.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
critic_watcher.py send_dm - 부분(소스/처리상태) Y Y(shared/sector)
cron_alert.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
cron_stall_detector.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
cron_watchdog.py send_dm - 부분(소스/처리상태) Y 불명
daily_intelligence_report.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
daily_system_validator.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
data_freshness_watcher.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
deep_dive_to_vault.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
dm_analyst_bot.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
domain_wiki_compiler.py send_dm - 부분(소스/처리상태) Y Y(shared/sector)
economic_calendar.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
eia_energy_collector.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
experiment_tracker.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
geopolitical_monitor.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
github_release_monitor.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
gmail_newsletter_collector.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
knowledge_promoter.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
kpi_telegram_wrapper.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
linkage_validator.py send_dm - 부분(소스/처리상태) Y Y(shared/sector)
memory_consolidate.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
methodology_conflict_detector.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
methodology_harvester.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
methodology_updater.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
oil_supply_backtest.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
pattern_explorer.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
petrochemical_cycle_tracker.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
pipeline_self_healer.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
quant_performance_tracker.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
regime_cycle_matrix.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
research_intelligence_aggregator.py send_sector - 부분(소스/처리상태) Y 불명
run_deep_analysis.py send_dm - N Y 불명
semi_market_data_collector.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
semiconductor_cycle_tracker.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
setup_dm_sectors.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
shipbuilding_cycle_tracker.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
signal_validator.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
skill_health.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
system_dashboard.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
system_digest.py send_sector - Y(발송/일자 해시·state) Y 불명
task_briefing.py send_dm/send_sector - 부분(소스/처리상태) Y Y(shared/sector)
technical_stat_models.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
telegram_briefing_wrapper.py send_sector - N Y 불명
thesis_tracker.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
upstream_tracker.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
url_enricher.py send_dm - 부분(소스/처리상태) Y Y(shared/sector)
vault_cleanup.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
vault_financial_bridge.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
vault_fundamental_bridge.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
vault_gdrive_backup.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)
vault_linker.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
vault_lint.py send_sector - N Y 불명
vault_lint_advanced.py send_sector - 부분(소스/처리상태) Y 불명
vault_macro_bridge.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
vault_reeval.py send_sector - 공통TTL만(1h exact) Y Y(shared/sector)
vault_technical_bridge.py send_sector - 부분(소스/처리상태) Y Y(shared/sector)
webapp_refresh.py send_dm - 공통TTL만(1h exact) Y Y(shared/sector)

5. 공통 유틸 제안

/Users/ron/.hermes/workspace/scripts/shared/dedup.py 추가 권고:

def should_send(namespace: str, text: str, ttl_hours: int = 24, meta: dict | None = None) -> tuple[bool, str]:
    """namespace별 last_sent hash를 ~/.hermes/workspace/memory/dedup/{namespace}.json에 저장/비교."""

def mark_sent(namespace: str, text: str, meta: dict | None = None) -> None:
    """전송 성공 후 hash, sent_at, meta를 원자적으로 기록."""

사용 패턴:

ok_to_send, reason = should_send("channel_collector", report, ttl_hours=24, meta={"job": job_id})
if not ok_to_send:
    log(f"dedup skip: {reason}")
    return
if send_sector("ideas", report, chunked=True):
    mark_sent("channel_collector", report)

설계 포인트: - sha256(normalized_text) 사용. 시각/런ID 같은 변동 헤더는 normalize 단계에서 제거 옵션 필요. - send_document는 문서 파일 bytes hash + caption hash를 같이 저장. - 기존 shared.telegram 1시간 TTL dedupe는 유지하되, 파이프라인 dedup은 최소 24시간/다음 데이터 변경 전까지 유지. - 빈 결과는 No new/0건일 때 전송하지 않는 정책을 공통화.

6. 증거 명령

cd ~/.hermes/workspace/scripts/pipeline
rg -n '\b(send_dm|send_sector|send_document)\s*\(' -g '*.py' .
python3 - <<'PY'  # ~/.hermes/cron/jobs.json에서 파일명별 크론 매칭
PY
sed -n '219,330p;670,780p;920,980p' ~/.hermes/workspace/scripts/shared/telegram.py

7. 자체평가

  • 정확성: 4.5/5 — 실제 grep, cron 매칭, 공통 전송부 확인 기반. 단, dedup 판정은 정적분석이라 런타임 분기까지 100% 보장하지는 않음.
  • 완성도: 4.5/5 — 전체 매트릭스와 TOP 5, 최소 수정 권고 포함.
  • 검증: 4.0/5 — 코드 실행 없이 읽기 감사만 수행하라는 요구에 맞춰 정적 검증 완료.
  • 최소 변경: 5/5 — 코드 수정 없음, 보고서만 작성.
  • 종합: 4.5/5

DONE