알림센터 발송 파이프라인 중복 체크 룰 감사
- 감사 시각: 2026-04-24 10:43:35 KST
- 범위:
/Users/ron/.hermes/workspace/scripts/pipeline/**/*.py중send_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 = 3600send_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.json에last_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 추가. 가능하면 customsend_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회.
seenarticle 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_hash를memory/bond-evening-concept/last_sent.json에 저장/비교. customsend_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