하트비트 및 라이프사이클 훅
2단계 결정 및 적응형 인터벌을 갖춘 HEARTBEAT.md 엔진과 훅/이벤트 파이프라인.
Revka에는 사용자 메시지 사이에 동작하고 모든 액션을 관찰하기 위한 두 가지 상호 보완적인 메커니즘이 있습니다. 하트비트 엔진은 revka daemon 내부에서 주기적인 백그라운드 루프를 실행하며, 사용자가 관리하는 HEARTBEAT.md 태스크 파일을 읽고 — 선택적으로 LLM을 활용하여 — 각 틱마다 실행할 태스크를 결정합니다. 훅 / 이벤트 파이프라인은 빌트인 핸들러(및 향후 플러그인)가 도구 호출, LLM 요청, 채널 메시지와 같은 라이프사이클 이벤트를 가로채고 반응할 수 있도록 하는 미들웨어 레이어입니다.
Revka가 주기적으로 상태를 확인하거나 무언가를 모니터링하거나 자율적인 에이전트 태스크를 반복 실행하기를 원할 때는 하트비트를 사용하세요. 감사 로깅, 컴플라이언스 전달, 에이전트 루프의 프로그래밍 방식 인터셉션이 필요할 때는 훅을 사용하세요. 두 기능 모두 ~/.revka/config.toml에서 설정하며, 감독된 데몬의 일부로 실행되므로 장애 발생 시 자동으로 재시작됩니다.
하트비트 엔진
섹션 제목: “하트비트 엔진”하트비트는 각 틱마다 다음을 수행하는 주기적 루프입니다.
- 워크스페이스에서
HEARTBEAT.md를 읽고 실행 가능한 태스크를 수집합니다(paused및completed항목을 제외하고 우선순위 순으로 정렬). - 관련 메모리와 선택적으로 최근 채널 기록을 불러옵니다.
- 2단계 모드에서는 선택된 태스크를 실행(2단계)하기 전에, 현재 실행할 만한 태스크가 있는지 LLM에게 묻습니다(1단계).
- 선택된 태스크를 실행하고 결과를 설정된 채널로 전달합니다.
- 실행 기록을 로컬 SQLite 기록 데이터베이스에 저장하고
HeartbeatTick옵저버 이벤트 및revka_heartbeat_ticks_total메트릭을 발행합니다.
엔진은 컴포넌트 헬스 레지스트리(GET /health)의 heartbeat 항목과 revka doctor의 daemon → heartbeat-freshness 체크(30초 후 stale 판정)를 담당하는 구성 요소입니다.
2단계 결정
섹션 제목: “2단계 결정”2단계 모드(two_phase = true, 기본값이자 권장 설정)는 각 틱을 두 번의 LLM 인터랙션으로 분리합니다.
- 1단계 — 결정. Revka가 실행 가능한 태스크를 모델에 제시하고 어떤 태스크를 실행할지 묻습니다. 모델은
run: 1,2,3으로 태스크 인덱스를 선택하거나,skip으로 아무것도 실행하지 않도록 응답합니다. - 2단계 — 실행. 선택된 태스크만 에이전트 방식으로 실행됩니다.
이 방식은 조용한 시간대의 API 비용을 절감합니다. 실행할 만한 태스크가 없는 틱에서는 모든 활성 태스크를 실행하는 대신 소규모 결정 호출 한 번만 비용이 발생합니다. two_phase = false로 설정하면 1단계를 완전히 건너뛰고 모든 활성 태스크를 매 틱마다 실행합니다 — 더 단순하지만 비용이 높고 노이즈가 많아집니다.
적응형 인터벌
섹션 제목: “적응형 인터벌”adaptive = false(기본값)일 때 하트비트는 매 interval_minutes마다 실행됩니다. adaptive = true일 때 인터벌은 min_interval_minutes와 max_interval_minutes 사이에서 유연하게 조정됩니다.
- 장애 시 백오프. 연속 장애 발생 후 다음 인터벌이 지수적으로 증가합니다:
interval = base * 2^consecutive_failures,max_interval_minutes에서 상한됨. - 긴급 작업 시 가속. 높은 우선순위의 태스크가 대기 중일 때 인터벌이
max(min_interval_minutes, 5)로 고정되어 긴급 항목이 다음 느린 틱을 기다리지 않도록 합니다.
엔진은 HeartbeatMetrics에 런타임 통계를 추적합니다: uptime_secs, consecutive_successes, consecutive_failures, last_tick_at, avg_tick_duration_ms(지수 이동 평균, α = 0.3), total_ticks.
데드맨 스위치
섹션 제목: “데드맨 스위치”데드맨 스위치는 하트비트가 틱을 멈췄을 때 — 데몬이 조용히 죽거나 멈춘 상태의 정식 신호 — 알림을 보내는 워치독입니다. deadman_timeout_minutes 이내에 틱이 발생하지 않으면 deadman_channel / deadman_to로 알림이 전달됩니다. 비활성화하려면 deadman_timeout_minutes = 0(기본값)으로 설정하세요.
[heartbeat]enabled = truedeadman_timeout_minutes = 90 # alert if no tick within 90 minutesdeadman_channel = "telegram"deadman_to = "123456789"하트비트 데몬 컴포넌트
섹션 제목: “하트비트 데몬 컴포넌트”revka daemon 내부에서 하트비트는 4개의 감독된 컴포넌트 중 하나로 실행됩니다(게이트웨이, 채널, 스케줄러와 함께). 워크스페이스에서 하트비트 태스크를 로드하고, 메모리를 불러오고, 선택적으로 1단계 결정 호출을 실행하고, 선택된 태스크를 실행하고, 결과를 설정된 채널로 전달하고, ~/.revka/workspace/heartbeat/ 아래에 실행 기록을 저장합니다.
알아두면 유용한 동작들:
- 전달 자동 감지.
target/to가 설정되지 않은 경우, 컴포넌트는allowed_users의 첫 번째 Telegram 항목을 전달 대상으로 자동 감지합니다. - 세션 컨텍스트.
load_session_context = true로 설정하면 엔진이 전달 대상의 세션 파일에서 마지막 20개 메시지를 각 태스크 프롬프트에 주입합니다. 사용자 메시지가 없으면 주입을 건너뜁니다. - 지연 트레이드오프. 2단계 모드는 결정 호출의 지연이 추가되지만 불필요한 실행을 줄입니다.
데몬의 컴포넌트 수퍼바이저 아래에서 실행되므로, 하트비트에서 크래시가 발생하면 지수 백오프 재시작이 트리거되고 컴포넌트 헬스 스냅샷의 재시작 횟수가 증가합니다 — 나머지 데몬은 종료되지 않습니다.
HEARTBEAT.md 형식
섹션 제목: “HEARTBEAT.md 형식”첫 실행 시 ensure_heartbeat_file()이 워크스페이스에 예시 태스크가 포함된 기본 HEARTBEAT.md를 생성합니다. 기존 파일은 절대 덮어쓰지 않으므로 직접 편집해도 안전합니다. 각 태스크는 Markdown 목록 항목입니다.
# Periodic Tasks
- [high] Check email for urgent messages- Review my calendar for today- [low|paused] Check the weather forecast- [completed] Old task각 줄은 - [priority|status] task text 형식을 따르며, 대괄호 접두사는 선택 사항입니다.
| 필드 | 값 | 기본값 | 의미 |
|---|---|---|---|
priority | high, medium, low | medium | 우선순위가 높은 태스크가 먼저 정렬되고 (적응형 모드에서) 인터벌을 단축합니다. |
status | active, paused, completed | active | paused 및 completed 태스크는 각 틱에서 제외됩니다. |
- task text만 있는 줄은 medium 우선순위와 active 상태로 처리됩니다. 두 필드를 |로 구분하여 대괄호 안에 함께 지정할 수 있습니다(예: [low|paused]). collect_runnable_tasks()는 paused 또는 completed인 항목을 모두 제외한 후 나머지를 우선순위 내림차순으로 정렬합니다.
태스크가 정의되지 않은 경우 엔진은 설정된 message(예: "Check in and summarize status")로 폴백하여 틱이 여전히 유용한 체크인을 생성합니다.
실행 기록
섹션 제목: “실행 기록”각 틱의 실행 기록은 <workspace>/heartbeat/history.db의 SQLite에 저장됩니다. 태스크별 출력은 16KB로 잘립니다. max_run_history 설정 키는 유지할 레코드 수를 제어하며, 새 레코드를 쓰는 것과 동일한 트랜잭션에서 오래된 레코드가 정리됩니다.
[heartbeat] 설정
섹션 제목: “[heartbeat] 설정”~/.revka/config.toml의 [heartbeat] 섹션에서 엔진을 설정합니다.
[heartbeat]enabled = trueinterval_minutes = 60 # base interval between tickstwo_phase = true # ask the LLM what to run first (recommended)adaptive = true # flex the interval; back off on failuremin_interval_minutes = 5 # adaptive floormax_interval_minutes = 240 # adaptive ceilingload_session_context = true # inject recent chat history into task promptsmax_run_history = 100 # SQLite run records to keepmessage = "Check in and summarize status" # fallback task if HEARTBEAT.md is emptytarget = "telegram" # delivery channelto = "123456789" # recipient (user ID / channel ID)deadman_timeout_minutes = 0 # 0 = disableddeadman_channel = "telegram"deadman_to = "123456789"| 키 | 타입 | 기본값 | 의미 |
|---|---|---|---|
enabled | bool | false | 하트비트 컴포넌트의 마스터 스위치. |
interval_minutes | int | 30 | 틱 사이의 기본 인터벌. |
two_phase | bool | true | 태스크 실행 전 1단계 LLM 결정을 실행합니다. |
adaptive | bool | false | 적응형 인터벌 백오프 / 가속을 활성화합니다. |
min_interval_minutes | int | — | 적응형 하한값 (및 높은 우선순위 태스크의 고정 인터벌). |
max_interval_minutes | int | — | 적응형 상한값 / 지수 백오프 상한. |
load_session_context | bool | false | 전달 대상의 마지막 20개 메시지를 프롬프트에 주입합니다. |
max_run_history | int | 100 | SQLite 기록 DB에 유지할 실행 레코드 수. |
message | string | — | HEARTBEAT.md에 실행 가능한 태스크가 없을 때의 폴백 태스크 텍스트. |
target | string | 자동 | 전달 채널 (설정되지 않은 경우 Telegram allowed_users[0] 자동 감지). |
to | string | 자동 | 전달 수신자 (사용자 ID / 채널 ID). |
deadman_timeout_minutes | int | 0 | 이 시간 내에 틱이 없으면 알림 발송. 0은 스위치를 비활성화합니다. |
deadman_channel | string | — | 데드맨 스위치 알림을 보낼 채널. |
deadman_to | string | — | 데드맨 스위치 알림 수신자. |
훅 / 이벤트 파이프라인
섹션 제목: “훅 / 이벤트 파이프라인”훅 시스템은 에이전트의 라이프사이클 이벤트를 가로채는 우선순위 기반 미들웨어 파이프라인입니다. 빌트인 핸들러(커맨드 로깅 및 웹훅 감사)의 확장 포인트이며 플러그인 작성자를 지원하도록 설계되었습니다. 훅에는 두 가지 종류가 있습니다.
- Void 훅 — fire-and-forget 알림으로, 병렬로 디스패치됩니다. 아무것도 변경할 수 없으며 관찰만 합니다. 예:
on_gateway_start,on_gateway_stop,on_session_start,on_session_end,on_llm_input,on_llm_output,on_after_tool_call,on_message_sent,on_heartbeat_tick. - 수정 훅 — 우선순위 내림차순으로 순차 실행되며, 각 훅의 출력이 다음 훅으로 전달됩니다. 값을 변경하거나 이벤트를 중단할 수 있습니다. 예:
before_model_resolve,before_prompt_build,before_llm_call,before_tool_call,on_message_received,on_message_sending.
수정 훅은 HookResult<T>를 반환합니다: Continue(value)는 (수정된) 값을 체인으로 전달하고, Cancel(reason)은 중단합니다. 첫 번째 Cancel이 체인을 단락시킵니다 — 해당 이벤트의 나머지 수정 훅은 실행되지 않습니다. Void 훅은 항상 모두 실행됩니다.
순서는 각 핸들러의 priority()로 제어됩니다: 높은 값이 수정 체인에서 먼저 실행됩니다(기본값은 0). 훅은 패닉 안전합니다 — 핸들러 내부의 패닉은 포착되며 에이전트를 크래시시키지 않습니다.
CommandLoggerHook (빌트인)
섹션 제목: “CommandLoggerHook (빌트인)”CommandLoggerHook은 모든 도구 호출이 완료된 후 도구 이름, 소요 시간(밀리초), 성공 여부를 구조화된 tracing::info! 출력으로 기록하는 빌트인 핸들러입니다.
[14:32:07] Bash (412ms) success=true우선순위 -50으로 실행되며(대부분의 다른 훅 이후) 별도의 설정이 필요하지 않습니다. 의존성 없는 경량 감사 로깅에 사용하세요 — 인터랙티브 세션을 위한 verbose 옵저버 백엔드나 tracing subscriber(예: RUST_LOG=info revka daemon)와 잘 어울립니다. 배포 환경에서 이 로그가 어디에 나타나는지는 관찰 가능성 및 트레이싱을 참조하세요.
WebhookAuditHook (빌트인)
섹션 제목: “WebhookAuditHook (빌트인)”중앙 집중식 감사, SIEM 수집 또는 컴플라이언스 로깅을 위해 WebhookAuditHook은 설정된 glob 패턴과 일치하는 모든 도구 호출에 대해 외부 HTTPS 엔드포인트로 JSON 페이로드를 POST합니다. 우선순위 -100(항상 마지막)으로 실행되며 fire-and-forget 방식입니다 — 전달 실패는 기록되지만 에이전트에 절대 전파되지 않습니다.
[hooks.webhook_audit] 아래에서 설정합니다.
[hooks.webhook_audit]enabled = trueurl = "https://audit.example.com/webhook"tool_patterns = ["Bash", "Write", "mcp__*"]include_args = truemax_args_bytes = 4096| 키 | 타입 | 기본값 | 의미 |
|---|---|---|---|
enabled | bool | false | 감사 훅의 마스터 스위치. |
url | string | — | 대상 엔드포인트. 반드시 https://여야 합니다(보안 참고 사항 참조). |
tool_patterns | list | [] | 감사할 도구의 glob 패턴. *는 어디서든 와일드카드로 사용됩니다(mcp__*, *_write, mcp__*__create). |
include_args | bool | false | 페이로드에 도구 인수를 포함합니다. |
max_args_bytes | int | 4096 | 포함할 직렬화된 인수의 최대 바이트 수. 0 = 무제한. |
POST 본문은 다음과 같습니다.
{ "event": "tool_call", "timestamp": "2026-06-18T14:32:07Z", "tool": "Bash", "success": true, "duration_ms": 42, "error": null, "args": { "command": "git status" }}include_args = false이거나 해당 호출에 캡처된 인수가 없는 경우 args는 null입니다. 인수는 before_tool_call에서 캡처되고 on_after_tool_call에서 사용됩니다.
[hooks] 설정
섹션 제목: “[hooks] 설정”[hooks] 섹션에는 라이프사이클 훅 설정과 빌트인 훅 토글이 포함됩니다. 웹훅 감사 훅은 [hooks.webhook_audit] 아래에 있습니다(위 참조). CommandLoggerHook은 훅이 활성화될 때 자동으로 등록되며 별도의 설정 키가 없습니다.
[hooks]
[hooks.webhook_audit]enabled = falseurl = "https://audit.example.com/webhook"tool_patterns = ["Bash", "Write", "mcp__*"]include_args = falsemax_args_bytes = 4096