[Design] 생존 판정: 살아 있지만 멈춘 에이전트(권한 확인 창 blocked, 종료 이벤트 없는 턴 종료)는 job 제한 시간으로만 감지됨 — blocked 알림·턴 종료 경고 제안 #30

Closed
opened 2026-09-15 05:32:10 +00:00 by Godopu · 3 comments
Owner

배경 — #18 생존 계약의 의도된 절충

#18 의 자동 heartbeat 타이머(lib_py/delegation/heartbeat.py)는 에이전트 프로세스에 묶여 있다(가장 가까운 에이전트 CLI 조상, 또는 --watch-pid). 그래서:

  • 에이전트 프로세스가 죽거나 kill 되면 한 주기 안에 heartbeat 가 멈추고 K×N 초 뒤 무응답(deadline_timeout)으로 닫힌다. 죽은 에이전트는 감지된다.
  • 살아 있지만 멈춘 에이전트는 heartbeat 를 계속 보낸다. docstring 과 .agents/MULTI_AGENT_RULES.md:172 가 이를 "계약의 의도된 절충(작업이 아닌 프로세스의 생존)"으로 명시한다. heartbeat 를 잊거나 긴 도구 호출 하나로 턴을 보내는 정상 에이전트를 무응답으로 닫던 문제(#14·#16)를 막기 위한 선택이고, TUI 스크래핑을 판정에서 배제한 것도 의도다.

이 이슈는 그 절충을 되돌리자는 것이 아니라, 절충이 남긴 운영 공백을 알림 수준으로 메우자는 제안이다.

관측 (2026-09-15, #21 단계 C — PR #29)

사례 상황 계약의 판정
planner idle + "2 shells still running" (job 1a16f9fd, 6d3d7776) 계획서 확정에 필요한 전체 테스트를 백그라운드로 돌리며 대기. 테스트가 끝나자 스스로 이어서 completed 발행 올바름. idle 을 멈춤으로 봤다면 정상 작업을 오탐으로 닫았을 것
creator-claude-issue20 가 구현 job 2380e6d1 중 Claude Code 권한 확인 창("Dangerous rm operation on possibly-empty variable path … Do you want to proceed?")에서 대기 herdr agent_status: blocked. heartbeat 는 계속 발행, 루프는 계속 대기 감지 불가. 사용자가 화면을 보고 먼저 발견했고 수동 승인으로 풀렸다

문제

루프·래퍼는 다음 세 상태를 구분하지 못한다.

  1. 살아서 일하는 중
  2. 살아 있지만 권한 확인 창·대화상자에서 멈춤 (blocked)
  3. 턴이 끝났는데 종료 이벤트(completed/error)를 보내지 않음 (입력창 대기)

2·3 은 job 의 벽시계 제한 시간(기본 --timeout 3600, 루프는 job 당 약 3900 초 대기)이 지나야 끝난다. 그동안 빨리 알아챌 수 있는 경로는 사람이 화면을 보는 것뿐이다.

제안 — 알림·진단 전용 (job 을 닫는 신호로 쓰지 않음, #18 원칙 유지)

  1. blocked 알림: herdr 가 에이전트를 blocked 로 B 초 이상 보고하면 진단 이벤트(예: progress --detail "agent blocked on a prompt")를 발행하고 루프·래퍼 로그에 경고한다. 오케스트레이터가 바로 확인할 수 있게 한다.
  2. 턴 종료 경고: 에이전트가 입력창 대기 상태이고, 자식 프로세스·백그라운드 셸이 없으며, 종료 이벤트 없이 X 분이 지나면 경고한다. 백그라운드 셸 조건은 위 첫 번째 사례의 오탐을 막기 위해 필수다.
  3. 문서: delegate-job SKILL.md 의 생존 계약 절에 "살아 있지만 멈춘 에이전트는 job 제한 시간으로만 감지된다"와 위 알림을 명시한다.

고려 사항:

  • herdr 상태·pane 정보는 #18 에서 판정이 아닌 진단 전용으로 격하됐다. 이 제안도 판정에 쓰지 않는 알림이다.
  • 원격 에이전트(transport: remote)에는 herdr pane 이 없어 1·2 를 적용할 수 없다. 원격은 runner 쪽 신호가 필요하며 별도 범위로 둔다.
  • herdr 의 agent_status 는 CLI 별로 정확도가 다르다(codex 는 working 을 보고하지 않음, #14). CLI 별 신뢰도를 문서화한다.

관련

  • #18 생존·전달 판정의 프로토콜 계약 (절충의 출처)
  • #16 전달 확인·생존 판정 빈틈 묶음, #17 운영 신뢰성 로드맵
  • #21 단계 C / PR #29 (관측 출처)

환경 정보

  • main 1134d74(v4.6.0) + PR #29, Linux, herdr 0.8.2, Claude Code v2.1.270
## 배경 — #18 생존 계약의 의도된 절충 #18 의 자동 heartbeat 타이머(`lib_py/delegation/heartbeat.py`)는 에이전트 **프로세스**에 묶여 있다(가장 가까운 에이전트 CLI 조상, 또는 `--watch-pid`). 그래서: - 에이전트 프로세스가 죽거나 kill 되면 한 주기 안에 heartbeat 가 멈추고 K×N 초 뒤 무응답(`deadline_timeout`)으로 닫힌다. **죽은 에이전트는 감지된다.** - 살아 있지만 멈춘 에이전트는 heartbeat 를 계속 보낸다. docstring 과 `.agents/MULTI_AGENT_RULES.md:172` 가 이를 "계약의 의도된 절충(작업이 아닌 프로세스의 생존)"으로 명시한다. heartbeat 를 잊거나 긴 도구 호출 하나로 턴을 보내는 정상 에이전트를 무응답으로 닫던 문제(#14·#16)를 막기 위한 선택이고, TUI 스크래핑을 판정에서 배제한 것도 의도다. 이 이슈는 그 절충을 되돌리자는 것이 아니라, **절충이 남긴 운영 공백**을 알림 수준으로 메우자는 제안이다. ## 관측 (2026-09-15, #21 단계 C — PR #29) | 사례 | 상황 | 계약의 판정 | |---|---|---| | planner `idle` + "2 shells still running" (job `1a16f9fd`, `6d3d7776`) | 계획서 확정에 필요한 전체 테스트를 백그라운드로 돌리며 대기. 테스트가 끝나자 스스로 이어서 completed 발행 | **올바름.** idle 을 멈춤으로 봤다면 정상 작업을 오탐으로 닫았을 것 | | `creator-claude-issue20` 가 구현 job `2380e6d1` 중 Claude Code 권한 확인 창("Dangerous rm operation on possibly-empty variable path … Do you want to proceed?")에서 대기 | herdr `agent_status: blocked`. heartbeat 는 계속 발행, 루프는 계속 대기 | **감지 불가.** 사용자가 화면을 보고 먼저 발견했고 수동 승인으로 풀렸다 | ## 문제 루프·래퍼는 다음 세 상태를 구분하지 못한다. 1. 살아서 일하는 중 2. 살아 있지만 권한 확인 창·대화상자에서 멈춤 (`blocked`) 3. 턴이 끝났는데 종료 이벤트(`completed`/`error`)를 보내지 않음 (입력창 대기) 2·3 은 job 의 벽시계 제한 시간(기본 `--timeout 3600`, 루프는 job 당 약 3900 초 대기)이 지나야 끝난다. 그동안 빨리 알아챌 수 있는 경로는 사람이 화면을 보는 것뿐이다. ## 제안 — 알림·진단 전용 (job 을 닫는 신호로 쓰지 않음, #18 원칙 유지) 1. **`blocked` 알림**: herdr 가 에이전트를 `blocked` 로 B 초 이상 보고하면 진단 이벤트(예: `progress --detail "agent blocked on a prompt"`)를 발행하고 루프·래퍼 로그에 경고한다. 오케스트레이터가 바로 확인할 수 있게 한다. 2. **턴 종료 경고**: 에이전트가 입력창 대기 상태이고, 자식 프로세스·백그라운드 셸이 없으며, 종료 이벤트 없이 X 분이 지나면 경고한다. 백그라운드 셸 조건은 위 첫 번째 사례의 오탐을 막기 위해 필수다. 3. **문서**: delegate-job `SKILL.md` 의 생존 계약 절에 "살아 있지만 멈춘 에이전트는 job 제한 시간으로만 감지된다"와 위 알림을 명시한다. 고려 사항: - herdr 상태·pane 정보는 #18 에서 판정이 아닌 진단 전용으로 격하됐다. 이 제안도 판정에 쓰지 않는 알림이다. - 원격 에이전트(`transport: remote`)에는 herdr pane 이 없어 1·2 를 적용할 수 없다. 원격은 runner 쪽 신호가 필요하며 별도 범위로 둔다. - herdr 의 `agent_status` 는 CLI 별로 정확도가 다르다(codex 는 working 을 보고하지 않음, #14). CLI 별 신뢰도를 문서화한다. ## 관련 - #18 생존·전달 판정의 프로토콜 계약 (절충의 출처) - #16 전달 확인·생존 판정 빈틈 묶음, #17 운영 신뢰성 로드맵 - #21 단계 C / PR #29 (관측 출처) ## 환경 정보 - main `1134d74`(v4.6.0) + PR #29, Linux, herdr 0.8.2, Claude Code v2.1.270
Author
Owner

설계 제안 — herdr Socket API 의 상태 이벤트로 "살아 있지만 멈춘" 상태를 알림 (병합 워크스페이스, 2026-09-25)

관측 (PR #35·#38 리뷰 중)

  • codex 리뷰어가 API 스트림 단절로 "Reconnecting… (2/5)" 에 3시간 갇혀 있는 동안 heartbeat 타이머는 30초마다 정상 발행됐고, 잡은 벽시계까지 열려 있었다. heartbeat 는 프로세스 생존만 증명한다(#18 의 의도된 절충).
  • 사람이 pane 을 보고 Esc 로 풀어 준 뒤에야 진행됐다. 이 개입을 자동화하는 것이 이 이슈의 목표다.

herdr Socket API(0.9.1 문서, https://herdr.dev/docs/socket-api/)에서 쓸 수 있는 것

  1. events.subscribe + pane.agent_status_changed: working/blocked/idle/done 전환을 서버가 밀어 준다. 필터(op: eq/in/…, pane_id, agent_status)로 특정 pane 의 blocked 만 받을 수 있다. 폴링 불필요.
  2. agent.wait --until blocked|idle --timeout <ms>: 서버 소유의 이벤트 기반 배리어. lib_py/loop/watcher.py 가 이미 감싸고 있다.
  3. state_change_seq: 상태 전환마다 오르는 순번. "S 초 동안 불변" = "상태 변화 없음". 대화 기록 파일 성장 같은 파일 기반 신호는 필요 없다.
  4. pane.read --source detection: herdr 의 화면 감지용 하단 버퍼 스냅샷. blocked 알림에 막힌 창의 문구를 붙일 때 쓴다.

문서가 주지 않는 것: 마지막 출력 시각, 턴·도구 호출 수명주기 이벤트, 막힌 창의 내용(그래서 4 가 필요). 상태 이벤트의 시간 메타데이터는 state_change_seq 뿐이다.

판정 규칙 (제안)

  • 래퍼가 구독자 옆에 감시자 하나를 더 두고 잡의 pane 에 pane.agent_status_changed 를 구독한다.
  • blocked 수신 → 즉시 progress --detail stalled:blocked 발행 + detection 스냅샷의 문구 첨부.
  • 잡이 열린 상태에서 idle 수신 → S 초(예: 10분) 타이머. 그 안에 working 으로 돌아오지 않으면(state_change_seq 불변) stalled:idle 발행 → 루프가 "이어서 하라" 프롬프트를 한 번 재주입.
  • working 인 동안은 heartbeat 만 와도 정상으로 본다. 위 codex 사례는 herdr 가 working 으로 보고하므로 이 규칙만으로는 못 잡는다 — 그 경우는 벽시계(#33)가 마지막 안전망이며, #33 의 연장 결정 때 state_change_seq 가 오래 멈춰 있으면 연장을 거부하는 식으로 보완한다.
  • 이 판정은 잡을 닫는 근거가 아니라 알림이다. #18 의 계약(프로토콜 이벤트만이 종결 근거)은 유지한다.

#33 과의 관계

  • #33: 벽시계 도달 시 래퍼가 닫지 않고 멈춰서 묻고(budget_exhausted 이벤트 + 결정 파일), 루프 정책 또는 오케스트레이터가 연장/거부, 거부면 wall_clock_timeout 으로 종결(무응답 deadline_timeout 과 구분). 연장 상한(기본 1회)은 유지.
  • 연장 근거의 정확도를 채우는 것이 이 이슈다. 두 이슈를 한 브랜치로 다루기를 권한다.

참고

  • 이 호스트(herdr 0.9.1)에서 codex 도 working 으로 보고된다. 0.9.0 의 "codex 항상 idle" 메모는 해소된 것으로 보인다.
## 설계 제안 — herdr Socket API 의 상태 이벤트로 "살아 있지만 멈춘" 상태를 알림 (병합 워크스페이스, 2026-09-25) ### 관측 (PR #35·#38 리뷰 중) - codex 리뷰어가 API 스트림 단절로 "Reconnecting… (2/5)" 에 3시간 갇혀 있는 동안 heartbeat 타이머는 30초마다 정상 발행됐고, 잡은 벽시계까지 열려 있었다. heartbeat 는 프로세스 생존만 증명한다(#18 의 의도된 절충). - 사람이 pane 을 보고 Esc 로 풀어 준 뒤에야 진행됐다. 이 개입을 자동화하는 것이 이 이슈의 목표다. ### herdr Socket API(0.9.1 문서, https://herdr.dev/docs/socket-api/)에서 쓸 수 있는 것 1. `events.subscribe` + `pane.agent_status_changed`: `working`/`blocked`/`idle`/`done` 전환을 서버가 **밀어 준다**. 필터(`op: eq/in/…`, `pane_id`, `agent_status`)로 특정 pane 의 `blocked` 만 받을 수 있다. 폴링 불필요. 2. `agent.wait --until blocked|idle --timeout <ms>`: 서버 소유의 이벤트 기반 배리어. `lib_py/loop/watcher.py` 가 이미 감싸고 있다. 3. `state_change_seq`: 상태 전환마다 오르는 순번. "S 초 동안 불변" = "상태 변화 없음". 대화 기록 파일 성장 같은 파일 기반 신호는 필요 없다. 4. `pane.read --source detection`: herdr 의 화면 감지용 하단 버퍼 스냅샷. `blocked` 알림에 막힌 창의 문구를 붙일 때 쓴다. 문서가 주지 않는 것: 마지막 출력 시각, 턴·도구 호출 수명주기 이벤트, 막힌 창의 내용(그래서 4 가 필요). 상태 이벤트의 시간 메타데이터는 `state_change_seq` 뿐이다. ### 판정 규칙 (제안) - 래퍼가 구독자 옆에 감시자 하나를 더 두고 잡의 pane 에 `pane.agent_status_changed` 를 구독한다. - **`blocked` 수신** → 즉시 `progress --detail stalled:blocked` 발행 + detection 스냅샷의 문구 첨부. - **잡이 열린 상태에서 `idle` 수신** → S 초(예: 10분) 타이머. 그 안에 `working` 으로 돌아오지 않으면(`state_change_seq` 불변) `stalled:idle` 발행 → 루프가 "이어서 하라" 프롬프트를 한 번 재주입. - **`working`** 인 동안은 heartbeat 만 와도 정상으로 본다. 위 codex 사례는 herdr 가 `working` 으로 보고하므로 이 규칙만으로는 못 잡는다 — 그 경우는 벽시계(#33)가 마지막 안전망이며, #33 의 연장 결정 때 `state_change_seq` 가 오래 멈춰 있으면 연장을 거부하는 식으로 보완한다. - 이 판정은 잡을 닫는 근거가 아니라 **알림**이다. #18 의 계약(프로토콜 이벤트만이 종결 근거)은 유지한다. ### #33 과의 관계 - #33: 벽시계 도달 시 래퍼가 닫지 않고 멈춰서 묻고(`budget_exhausted` 이벤트 + 결정 파일), 루프 정책 또는 오케스트레이터가 연장/거부, 거부면 `wall_clock_timeout` 으로 종결(무응답 `deadline_timeout` 과 구분). 연장 상한(기본 1회)은 유지. - 연장 근거의 정확도를 채우는 것이 이 이슈다. 두 이슈를 한 브랜치로 다루기를 권한다. ### 참고 - 이 호스트(herdr 0.9.1)에서 codex 도 `working` 으로 보고된다. 0.9.0 의 "codex 항상 idle" 메모는 해소된 것으로 보인다.
Author
Owner

작업 시 감안할 점 — #17 항목 4 의 경계 기준 (병합 워크스페이스, 2026-09-25)

위 설계 제안(herdr events.subscribe / pane.agent_status_changed / state_change_seq / pane.read --source detection)을 구현할 때:

  • 경계 기준(#17 댓글 참조): 결정을 내리는 코드는 Python, 외부 프로세스를 만지는 코드는 bash. 상태 이벤트 구독 감시자와 "멈춤" 판정은 Python 에 둔다(job_subscriber.py 옆, lib_py/loop/watcher.py 의 agent wait 래핑 재사용). bash 에는 감시자 기동과 다이얼로그 키 주입(#24 의 handle_startup_dialogs)만 남긴다.
  • 순서: #17 항목 4 가 래퍼의 종결 처리를 Python 으로 옮긴 뒤 이 이슈와 #33 을 같은 브랜치에서 다루기를 권한다. #33 의 벽시계 연장 결정이 이 이슈의 판정을 입력으로 쓴다.
  • 계약 유지: 이 판정은 알림이지 종결 근거가 아니다(#18). stalled:blocked / stalled:idle 은 progress 이벤트로 발행한다.
  • 대화 기록 파일 성장 신호는 쓰지 않는다(상태 순번 state_change_seq 로 대체).
## 작업 시 감안할 점 — #17 항목 4 의 경계 기준 (병합 워크스페이스, 2026-09-25) 위 설계 제안(herdr `events.subscribe` / `pane.agent_status_changed` / `state_change_seq` / `pane.read --source detection`)을 구현할 때: - **경계 기준(#17 댓글 참조)**: 결정을 내리는 코드는 Python, 외부 프로세스를 만지는 코드는 bash. 상태 이벤트 구독 감시자와 "멈춤" 판정은 Python 에 둔다(`job_subscriber.py` 옆, `lib_py/loop/watcher.py` 의 `agent wait` 래핑 재사용). bash 에는 감시자 기동과 다이얼로그 키 주입(#24 의 `handle_startup_dialogs`)만 남긴다. - **순서**: #17 항목 4 가 래퍼의 종결 처리를 Python 으로 옮긴 뒤 이 이슈와 #33 을 같은 브랜치에서 다루기를 권한다. #33 의 벽시계 연장 결정이 이 이슈의 판정을 입력으로 쓴다. - **계약 유지**: 이 판정은 알림이지 종결 근거가 아니다(#18). `stalled:blocked` / `stalled:idle` 은 `progress` 이벤트로 발행한다. - **대화 기록 파일 성장 신호는 쓰지 않는다**(상태 순번 `state_change_seq` 로 대체).
Author
Owner

PR #41 병합(v4.8.0)으로 닫힙니다. 리뷰어 네 명(claude/agy/grok/codex) 모두 갱신 1(알림 전용: 자동 수락 없음, stalled 판정으로 닫지 않음)에 동의했습니다. stall_watch.py 의 유일한 발행은 progress(stalled:blocked <문구> / stalled:idle + 1회 nudge)이며 AST 테스트(S12)가 이를 강제합니다. 종결은 #33 의 두 시계뿐입니다.

리뷰에서 잡힌 실행 안전성 결함 세 건도 함께 고쳤습니다: 래퍼가 감시자를 멈출 때 무한 대기(TERM 이 fork-exec 창에서 삼켜짐 → TERM/5 s/KILL), 래퍼가 죽으면 감시자 고아(--watch-pid), herdr 대기 중 닫히거나 재디스패치된 잡에 재주입(nudge 직전 레코드 재검사). 후속: MAM_TEST_UNRELIABLE_AGENT 테스트 훅 제거(테스트 seam 필요), 감시 여부 판정·수명 관리의 Python 이전은 #17 항목 4.

PR #41 병합(v4.8.0)으로 닫힙니다. 리뷰어 네 명(claude/agy/grok/codex) 모두 갱신 1(알림 전용: 자동 수락 없음, `stalled` 판정으로 닫지 않음)에 동의했습니다. `stall_watch.py` 의 유일한 발행은 `progress`(`stalled:blocked <문구>` / `stalled:idle` + 1회 nudge)이며 AST 테스트(S12)가 이를 강제합니다. 종결은 #33 의 두 시계뿐입니다. 리뷰에서 잡힌 실행 안전성 결함 세 건도 함께 고쳤습니다: 래퍼가 감시자를 멈출 때 무한 대기(TERM 이 fork-exec 창에서 삼켜짐 → TERM/5 s/KILL), 래퍼가 죽으면 감시자 고아(`--watch-pid`), herdr 대기 중 닫히거나 재디스패치된 잡에 재주입(nudge 직전 레코드 재검사). 후속: `MAM_TEST_UNRELIABLE_AGENT` 테스트 훅 제거(테스트 seam 필요), 감시 여부 판정·수명 관리의 Python 이전은 #17 항목 4.
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#30