[Design] 생존·전달 판정을 프로토콜 계약(heartbeat/started/completed)으로 완결 — TUI 스크래핑 배제, 원격 에이전트 통합의 선결 조건 #18

Closed
opened 2026-09-13 02:08:20 +00:00 by Godopu · 1 comment
Owner

목표 (Goal)

에이전트의 생존·전달 판정을 TUI 스크래핑(herdr agent_status, pane 문자열, 화면 변화)에서 떼어내어, 에이전트가 스스로 발행하는 이벤트(started/progress/completed/error) 계약으로 완결한다. 이는 #17 로드맵의 1번 항목을 독립시킨 것으로, 오케스트레이터와 다른 호스트에서 도는 원격 에이전트 통합의 선결 조건이다.

왜 지금인가 (Why)

  • #14 계열 수정(send_keys_safe 전달 확인, _inject_acknowledged, _agent_still_working, _recent_disk_heartbeat, record_local_event)은 같은 호스트에서는 효과가 있었지만 전부 휴리스틱을 한 겹씩 더 쌓는 방식이었다: herdr 상태(codex 는 항상 idle) → pane esc to interrupt → 화면 변화 → 디스크 heartbeat → lsof → ps. 문자열 계약이 없고, CLI 버전이 바뀌면 조용히 깨지며, 타이밍 경합(codex 의 1~2초 턴)이 있고, 틀려도 예외가 아니라 잘못된 판정으로 나타난다.
  • 이 안전장치들은 오케스트레이터의 디스크(events.log, 레지스트리)와 로컬 herdr pane 을 전제한다. 원격 에이전트는 둘 다 공유하지 않으므로 원격에서는 와이어(구독자)가 유일한 신호가 되고, 오늘 구독자가 idle 120초에 살아 있는 job 을 닫은 사례(fa90cc35, 9c6eb8b8, e503701e)가 원격에서는 그대로 재현된다.
  • 반대 근거도 있다: 오늘 브리프로 heartbeat 를 60초 이내로 강제하고 디스크 heartbeat 를 판정에 넣은 뒤로는 루프가 안정적으로 수렴했다. 프로토콜 신호가 충분하면 스크래핑 없이도 된다는 뜻이다.

계약 (Contract)

새 이벤트 종류를 만들지 않는다. 기존 이벤트를 의무·주기·타임아웃 규칙이 있는 계약으로 격상한다.

규칙 내용
heartbeat 의무 에이전트는 작업 중 N초(기본 30s) 이내마다 progress 를 발행한다. 브리프 문장에 기대지 않고 에이전트 쪽 헬퍼가 자동 발행한다(예: publish_event.py --heartbeat <N> 백그라운드 타이머, 또는 래퍼가 주입하는 시작 명령이 타이머를 띄움). 에이전트가 잊어도 된다.
응답 없음 판정 오케스트레이터는 마지막 이벤트로부터 K×N초(기본 3회 미수신 = 90s) 가 지나면 무응답으로 본다. 이 판정에 herdr 상태·pane·디스크는 쓰지 않는다.
전달 확인 프롬프트 주입 후 T초(기본 60s) 안에 started 가 없으면 전달 실패로 fail-closed(재주입 없음 — 단일 주입 계약 유지). pane 변화·busy 토큰은 전달 판정에서 제외.
종료 판정 completed/error 만이 종료다. 터미널 상태 불변(Issue #6 CAS)은 유지.
와이어 내구성 heartbeat QoS 1, job 상태는 retained 토픽(예: <topic_prefix>/state)으로도 발행해 재접속 시 즉시 복구, seq 갭으로 유실 감지, 구독자는 재접속 시 자동 재구독하고 progress 를 포함한 모든 메시지로 idle 창을 리셋.
자격 증명 각 호스트가 자기 .mam.env 로 브로커에 접속(#12 와 정합), job 별 auth_token(HMAC) 유지.
TUI 스크래핑의 남는 자리 (a) 프롬프트 주입과 trust/permission 다이얼로그 처리처럼 화면을 봐야만 하는 조작, (b) 같은 호스트에서 이벤트가 끊겼을 때 "죽었는지 멈췄는지"를 구분하는 진단 정보(로그에 남기되 판정에는 쓰지 않음). 남는 스크래핑은 CLI 버전별 계약 테스트(test_a4, test_c1 이 씨앗)로 고정한다.

구현 범위 (Scope)

  • lib_py/delegation/publish_event.py: --heartbeat <N> (백그라운드 타이머, 부모 종료 시 함께 종료, 같은 dispatch_token/attempt_id 로 서명).
  • 래퍼 multi-agent-mux-delegate-job 의 주입 명령: 에이전트에게 heartbeat 헬퍼 기동을 포함한 시작 명령을 전달. _inject_acknowledged 는 "started 수신"으로, R1 유예(_agent_still_working·_recent_disk_heartbeat)는 "마지막 이벤트로부터 K×N" 규칙으로 단순화.
  • lib_py/delegation/job_subscriber.py: 재접속·재구독, seq 갭 감지, idle 창 리셋 규칙, retained 상태 토픽 구독.
  • lib.sh send_keys_safe: 전달 확인을 started 대기로 교체(pane 변화 신호 제거). herdr agent prompt 는 주입 수단으로만.
  • lib_py/loop/runner.py: N·K·T 를 job 등록 시 전달(--idle-timeout 을 K×N 으로 유도).
  • 원격 세션 어댑터(또는 agent_session 이 herdr: 가 아닌 경우)에서는 herdr·pane·lsof·ps 경로를 건너뛰는 분기.
  • 문서: MESSAGING.md(계약 표), MULTI_AGENT_RULES.md Timeout 절, 8개 SKILL.md 의 관련 서술.

수용 기준 (Acceptance)

  1. herdr 상태가 항상 idle 이고 pane 이 비어 있어도(헤드리스), heartbeat 만으로 K×N 동안 job 이 살아 있고 completed 를 받으면 rc 0.
  2. 주입 후 T초 안에 started 가 없으면 rc 1(전달 실패), 재주입 없음, 레지스트리 error.
  3. heartbeat 가 K×N 을 넘겨 끊기면 deadline_timeout 으로 닫히고, 그 뒤 도착한 completed 는 거부(기존 CAS).
  4. 브로커 재접속 후 retained 상태로 즉시 복구되고, seq 갭이 로그·감사 기록에 남는다.
  5. 오케스트레이터 디스크를 공유하지 않는 "원격" 시뮬레이션(별도 tmp HOME, 별도 프로세스, 브로커만 공유)으로 --all-reviewer 루프가 수렴한다.
  6. send_keys_safe 의 pane 변화·busy 토큰 신호 제거 후에도 test_issue14_delivery_ack.py 의 fail-closed 계약(무신호 rc 4 등)은 started 기반으로 동치 유지.

우선순위

High. #17 항목 2(정족수)와 함께 루프 수렴 시간에 직결되고, 원격 통합의 전제이다. #17 항목 4(셸 계층 축소)는 이 작업이 자리 잡은 뒤 진행.

관련

  • #17 로드맵(항목 1 → 본 이슈로 분리), #16 전달 확인·생존 판정 빈틈(항목 2·3·4·8 흡수), #14(closed) 같은 호스트 해결분, #12(closed 예정) 호스트별 자격 증명, IMPROVEMENTS.md A-2.
## 목표 (Goal) 에이전트의 **생존·전달 판정을 TUI 스크래핑(herdr `agent_status`, pane 문자열, 화면 변화)에서 떼어내어, 에이전트가 스스로 발행하는 이벤트(`started`/`progress`/`completed`/`error`) 계약으로 완결**한다. 이는 #17 로드맵의 1번 항목을 독립시킨 것으로, 오케스트레이터와 다른 호스트에서 도는 **원격 에이전트 통합의 선결 조건**이다. ## 왜 지금인가 (Why) - #14 계열 수정(`send_keys_safe` 전달 확인, `_inject_acknowledged`, `_agent_still_working`, `_recent_disk_heartbeat`, `record_local_event`)은 같은 호스트에서는 효과가 있었지만 전부 **휴리스틱을 한 겹씩 더 쌓는** 방식이었다: herdr 상태(codex 는 항상 `idle`) → pane `esc to interrupt` → 화면 변화 → 디스크 heartbeat → `lsof` → `ps`. 문자열 계약이 없고, CLI 버전이 바뀌면 조용히 깨지며, 타이밍 경합(codex 의 1~2초 턴)이 있고, 틀려도 예외가 아니라 잘못된 판정으로 나타난다. - 이 안전장치들은 **오케스트레이터의 디스크**(`events.log`, 레지스트리)와 **로컬 herdr pane** 을 전제한다. 원격 에이전트는 둘 다 공유하지 않으므로 원격에서는 와이어(구독자)가 유일한 신호가 되고, 오늘 구독자가 idle 120초에 살아 있는 job 을 닫은 사례(`fa90cc35`, `9c6eb8b8`, `e503701e`)가 원격에서는 그대로 재현된다. - 반대 근거도 있다: 오늘 브리프로 heartbeat 를 60초 이내로 강제하고 디스크 heartbeat 를 판정에 넣은 뒤로는 루프가 안정적으로 수렴했다. 프로토콜 신호가 충분하면 스크래핑 없이도 된다는 뜻이다. ## 계약 (Contract) 새 이벤트 종류를 만들지 않는다. 기존 이벤트를 **의무·주기·타임아웃 규칙이 있는 계약**으로 격상한다. | 규칙 | 내용 | |---|---| | **heartbeat 의무** | 에이전트는 작업 중 N초(기본 30s) 이내마다 `progress` 를 발행한다. 브리프 문장에 기대지 않고 **에이전트 쪽 헬퍼가 자동 발행**한다(예: `publish_event.py --heartbeat <N>` 백그라운드 타이머, 또는 래퍼가 주입하는 시작 명령이 타이머를 띄움). 에이전트가 잊어도 된다. | | **응답 없음 판정** | 오케스트레이터는 마지막 이벤트로부터 K×N초(기본 3회 미수신 = 90s) 가 지나면 무응답으로 본다. 이 판정에 herdr 상태·pane·디스크는 쓰지 않는다. | | **전달 확인** | 프롬프트 주입 후 T초(기본 60s) 안에 `started` 가 없으면 전달 실패로 fail-closed(재주입 없음 — 단일 주입 계약 유지). pane 변화·busy 토큰은 전달 판정에서 제외. | | **종료 판정** | `completed`/`error` 만이 종료다. 터미널 상태 불변(Issue #6 CAS)은 유지. | | **와이어 내구성** | heartbeat QoS 1, job 상태는 retained 토픽(예: `<topic_prefix>/state`)으로도 발행해 재접속 시 즉시 복구, `seq` 갭으로 유실 감지, 구독자는 재접속 시 자동 재구독하고 `progress` 를 포함한 모든 메시지로 idle 창을 리셋. | | **자격 증명** | 각 호스트가 자기 `.mam.env` 로 브로커에 접속(#12 와 정합), job 별 `auth_token`(HMAC) 유지. | | **TUI 스크래핑의 남는 자리** | (a) 프롬프트 주입과 trust/permission 다이얼로그 처리처럼 화면을 봐야만 하는 조작, (b) 같은 호스트에서 이벤트가 끊겼을 때 "죽었는지 멈췄는지"를 구분하는 **진단 정보**(로그에 남기되 판정에는 쓰지 않음). 남는 스크래핑은 CLI 버전별 계약 테스트(`test_a4`, `test_c1` 이 씨앗)로 고정한다. | ## 구현 범위 (Scope) - `lib_py/delegation/publish_event.py`: `--heartbeat <N>` (백그라운드 타이머, 부모 종료 시 함께 종료, 같은 `dispatch_token`/`attempt_id` 로 서명). - 래퍼 `multi-agent-mux-delegate-job` 의 주입 명령: 에이전트에게 heartbeat 헬퍼 기동을 포함한 시작 명령을 전달. `_inject_acknowledged` 는 "`started` 수신"으로, R1 유예(`_agent_still_working`·`_recent_disk_heartbeat`)는 "마지막 이벤트로부터 K×N" 규칙으로 단순화. - `lib_py/delegation/job_subscriber.py`: 재접속·재구독, `seq` 갭 감지, idle 창 리셋 규칙, retained 상태 토픽 구독. - `lib.sh send_keys_safe`: 전달 확인을 `started` 대기로 교체(pane 변화 신호 제거). herdr `agent prompt` 는 주입 수단으로만. - `lib_py/loop/runner.py`: N·K·T 를 job 등록 시 전달(`--idle-timeout` 을 K×N 으로 유도). - 원격 세션 어댑터(또는 `agent_session` 이 `herdr:` 가 아닌 경우)에서는 herdr·pane·`lsof`·`ps` 경로를 건너뛰는 분기. - 문서: `MESSAGING.md`(계약 표), `MULTI_AGENT_RULES.md` Timeout 절, 8개 SKILL.md 의 관련 서술. ## 수용 기준 (Acceptance) 1. herdr 상태가 항상 `idle` 이고 pane 이 비어 있어도(헤드리스), heartbeat 만으로 K×N 동안 job 이 살아 있고 `completed` 를 받으면 rc 0. 2. 주입 후 T초 안에 `started` 가 없으면 rc 1(전달 실패), 재주입 없음, 레지스트리 `error`. 3. heartbeat 가 K×N 을 넘겨 끊기면 `deadline_timeout` 으로 닫히고, 그 뒤 도착한 `completed` 는 거부(기존 CAS). 4. 브로커 재접속 후 retained 상태로 즉시 복구되고, `seq` 갭이 로그·감사 기록에 남는다. 5. 오케스트레이터 디스크를 공유하지 않는 "원격" 시뮬레이션(별도 tmp HOME, 별도 프로세스, 브로커만 공유)으로 `--all-reviewer` 루프가 수렴한다. 6. `send_keys_safe` 의 pane 변화·busy 토큰 신호 제거 후에도 `test_issue14_delivery_ack.py` 의 fail-closed 계약(무신호 rc 4 등)은 `started` 기반으로 동치 유지. ## 우선순위 **High.** #17 항목 2(정족수)와 함께 루프 수렴 시간에 직결되고, 원격 통합의 전제이다. #17 항목 4(셸 계층 축소)는 이 작업이 자리 잡은 뒤 진행. ## 관련 - #17 로드맵(항목 1 → 본 이슈로 분리), #16 전달 확인·생존 판정 빈틈(항목 2·3·4·8 흡수), #14(closed) 같은 호스트 해결분, #12(closed 예정) 호스트별 자격 증명, `IMPROVEMENTS.md` A-2.
Author
Owner

main 5611859 에 병합되어 해결되었습니다 (v4.5.0, 브랜치 fix/issue-18).

계약 (MESSAGING.md §2.5)

  • heartbeat 의무: publish_event --event started --heartbeat N(기본 30)이 분리 타이머로 progress를 자동 발행. 타이머는 터미널 이벤트(seq lock 아래 barrier + drain), 재dispatch, timeout_sec, 감시 대상 에이전트 프로세스 소멸(--watch-pid, 기본은 가장 가까운 에이전트 CLI 조상) 시 멈추고, 잡별 lifecycle lock 으로 소유권이 직렬화됩니다.
  • 무응답 판정: 마지막 이벤트로부터 K×N(기본 4×30=120 s, --idle-timeout override). herdr 상태·pane 은 진단 로그로만 남깁니다.
  • 전달 확인: 주입 후 T(기본 180 s) 내 started 없으면 rc 1 agent_blocked, 재주입 없음. send_keys_safe 의 pane 변화·busy 토큰 신호 제거.
  • 와이어 내구성: 이벤트마다 같은 연결로 retained <prefix>/state; 구독자는 재접속 시 터미널 복구, seq 갭·재접속 감사 기록, seq 가 진전된 state 만 idle 갱신.
  • 원격 전송: --agent-session remote:<name> → 서명(MAM_DISPATCH_SECRET)된 retained dispatch, remote.py run 러너가 자기 레지스트리에 미러링·실행·터미널 발행, 구독자 --remote 가 오케스트레이터 레지스트리에 반영. attempt 원자적 admission, 명령 프로세스 그룹 종료.
  • 루프 러너: MAM_HEARTBEAT_SEC/MAM_HEARTBEAT_MISSES/MAM_ACK_TIMEOUT_S 전달, YAML transport: remote 행 → remote:<name>.

수용 기준: 1·2·3·4·6 테스트로 검증(tests/test_issue18_liveness_contract.py 28건). 5는 브로커만 공유하는 원격 러너의 단일 잡 시뮬레이션과 러너 라벨 단위 테스트까지 검증했고, 원격 리뷰어를 포함한 --all-reviewer 루프 수렴은 실제 원격 호스트 환경에서 확인이 필요합니다. 레거시 MAM_LOOP_PY=0 경로는 원격 미지원(문서화).

리뷰: claude·agy·grok·codex 4인 [VERDICT: PASS](최종 HEAD 263d6bf). 전체 스위트 906 통과. 후속: #20(테스트 감사 로그 위생).

main `5611859` 에 병합되어 해결되었습니다 (v4.5.0, 브랜치 `fix/issue-18`). **계약 (MESSAGING.md §2.5)** - heartbeat 의무: `publish_event --event started --heartbeat N`(기본 30)이 분리 타이머로 `progress`를 자동 발행. 타이머는 터미널 이벤트(seq lock 아래 barrier + drain), 재dispatch, `timeout_sec`, 감시 대상 에이전트 프로세스 소멸(`--watch-pid`, 기본은 가장 가까운 에이전트 CLI 조상) 시 멈추고, 잡별 lifecycle lock 으로 소유권이 직렬화됩니다. - 무응답 판정: 마지막 이벤트로부터 K×N(기본 4×30=120 s, `--idle-timeout` override). herdr 상태·pane 은 진단 로그로만 남깁니다. - 전달 확인: 주입 후 T(기본 180 s) 내 `started` 없으면 rc 1 `agent_blocked`, 재주입 없음. send_keys_safe 의 pane 변화·busy 토큰 신호 제거. - 와이어 내구성: 이벤트마다 같은 연결로 retained `<prefix>/state`; 구독자는 재접속 시 터미널 복구, seq 갭·재접속 감사 기록, seq 가 진전된 state 만 idle 갱신. - 원격 전송: `--agent-session remote:<name>` → 서명(`MAM_DISPATCH_SECRET`)된 retained dispatch, `remote.py run` 러너가 자기 레지스트리에 미러링·실행·터미널 발행, 구독자 `--remote` 가 오케스트레이터 레지스트리에 반영. attempt 원자적 admission, 명령 프로세스 그룹 종료. - 루프 러너: `MAM_HEARTBEAT_SEC/MAM_HEARTBEAT_MISSES/MAM_ACK_TIMEOUT_S` 전달, YAML `transport: remote` 행 → `remote:<name>`. **수용 기준**: 1·2·3·4·6 테스트로 검증(`tests/test_issue18_liveness_contract.py` 28건). 5는 브로커만 공유하는 원격 러너의 단일 잡 시뮬레이션과 러너 라벨 단위 테스트까지 검증했고, 원격 리뷰어를 포함한 `--all-reviewer` 루프 수렴은 실제 원격 호스트 환경에서 확인이 필요합니다. 레거시 `MAM_LOOP_PY=0` 경로는 원격 미지원(문서화). **리뷰**: claude·agy·grok·codex 4인 `[VERDICT: PASS]`(최종 HEAD 263d6bf). 전체 스위트 906 통과. 후속: #20(테스트 감사 로그 위생).
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tmpl/multi-agent-mux#18