Files
multi-agent-mux/.agents/reports/issue_6_consolidated_review_report.md
T

11 KiB

📑 Issue #6 종합 리뷰 및 아키텍처 개선 통합 보고서 (Rev. 2)

  • 작성 주체: 오케스트레이터 (Orchestrator)
  • 참여 리뷰어: planner-reviewer-claude-01 (Claude), reviewer-creator-grok-01 (Grok), planner-reviewer-codex-01 (Codex), reviewer-opencode-01 (OpenCode)
  • 검토 대상: Gitea Issue #6 — [Bug] delegate-job 워커 실패 시 레지스트리 미갱신(Fail-Silent) 및 TUI 턴 마감 레이스 컨디션 해결
  • 검토 기준 커밋: f98b958 (main, refactor, v4.4.0)
  • 문서 버전: Revision 2 (Rev. 2 — 2차 라운드 리뷰어 피드백 전건 반영)

1. 개요 및 총평

오케스트레이터 루프(run_loop.sh / lib_py.loop) 실행 중 발생했던 3,900초 영구 대기(hang) 결함(Issue #6)에 대해 4명의 리뷰어가 독립적으로 소스 코드 추적, 단위 격리 재현, 실측 포렌식, Herdr 0.8.2 네이티브 API 계약 분석을 수행했습니다.

리뷰어 4인 전원의 만장일치 판정 결과:

"Issue #6에서 제기한 결함 ①과 결함 ②는 모두 실제 코드베이스에 존재하는 심각한 실재 버그 및 설계 결함이다."

특히 2차 라운드 교차 검토를 통해 루프 실사용 경로(--type direct)의 숨은 결함, 기존 deadline_timeout CAS 엔진 재사용 원칙, 구독자 타임아웃 예산 자가포식 방지를 위한 사전 핸드셰이크 순서, 브로커 장애(rc=3) 격리 등 프로덕션 수준의 정밀한 가드레일이 통합 보고서 Rev.2에 완벽히 보강되었습니다.


2. 결함별 진위 검토 및 심층 분석

2.1 결함 ①: multi-agent-mux-delegate-job 워커 실패 시 레지스트리 미갱신 (Fail-Silent 버그)

  • 판정: 실재하는 핵심 결함 (CONFIRMED REAL, P0/P1)
  • 실측 및 재현 결과:
    1. 이슈 지목 위치(:358-362)의 진위:
      • _phase == "worker"에서 job_status != "completed"일 때 break 후 레지스트리(registry.py status --set error) 갱신 없이 함수 끝에서 로그 경로를 출력하며 exit 0으로 종료되는 현상이 정확히 확인되었습니다.
      • 동일한 결함이 리뷰어 단계(:375-378) 조기 탈출 분기에도 동일하게 존재합니다.
    2. 오케스트레이터 실사용 경로(--type direct:227)의 숨은 결함 (전원 공통 발견):
      • 상위 오케스트레이터(lib_py/loop/runner.py:271 및 run_loop.sh)는 루프 내부에서 --type loop가 아닌 **--type direct**로 에이전트 작업을 위임합니다.
      • direct 경로(:227)는 run_agent 실행 후 wait "$sub_pid" || true로 구독자 실패를 삼켜버리고, 상태 전이를 전혀 거치지 않은 채 exit 0을 반환합니다.
      • 따라서 이슈가 언급한 루프 분기뿐만 아니라 **오케스트레이터가 실제로 타는 direct 경로 역시 완전히 침묵(Fail-Silent)**하고 있었습니다.
    3. 파급 효과 (65분 Hang):
      • 상위 루프의 wait_for_job은 completed, error, cancelled 3개 터미널 상태만 종료 조건으로 인식합니다.
      • 구독자가 120초 idle timeout(sub_rc=2)으로 죽었음에도 레지스트리가 pending 또는 running으로 방치되어 상위 루프가 기본값인 3,900초(65분) 동안 블로킹됩니다.
      • 실제 운영 레지스트리 포렌식 결과 93a44347, 2961b822, ee242671 3건의 실운영 작업이 정확히 이 결함으로 장시간 교착 상태에 방치되었던 사실이 실증되었습니다.

2.2 결함 ②: 에이전트 이벤트 발행과 TUI 복귀 간의 레이스 컨디션

  • 판정: 실재하는 설계 결함 (CONFIRMED REAL, P1)
  • 실측 및 재현 결과:
    1. 비동기 상태 불일치 레이스:
      • 에이전트 프로세스가 MQTT로 completed 이벤트를 발행한 직후에도 터미널 화면은 마크다운 렌더링, 버퍼 플러시, 툴 호출 정리 등으로 인해 여전히 working/busy 상태일 수 있습니다.
      • 실측 결과 Codex 등 대형 모델의 경우 completed 발행 후 TUI가 done으로 떨어지는 데 최대 31초가 소요되기도 했습니다.
    2. 프롬프트 묵살 및 유실:
      • 다음 단계(리뷰어 등)가 즉시 프롬프트를 전송하면 TUI가 입력을 무시하거나 첫 글자/단어가 잘린 채 주입되어 문맥이 깨진 채 실행되거나 무응답 상태에 빠집니다.
    3. 두 결함 간의 악순환 증폭:
      • 결함 ②(TUI 미준비 프롬프트 유실) 발생 → 워커 무응답 → 구독자 120초 타임아웃(sub_rc=2) → 결함 ①(Fail-Silent) 발동 → 레지스트리 pending/running 방치 → 상위 루프 65분 영구 교착(Hang).

3. 수정 난이도 및 공수 산정 (Consensus)

영역 난이도 예상 공수 핵심 변경 대상
결함 ① (Fail-Closed CAS) 중하 (Low-Med) 1.0 ~ 1.5 MD skills/.../multi-agent-mux-delegate-job, lib_py/delegation/engine.py
결함 ② (TUI 턴 핸드셰이크) 중 (Medium) 1.0 ~ 1.5 MD .agents/skills/multi-agent-mux-create/scripts/lib.sh (send_keys_safe), Herdr API 연동
통합 테스트 & 다중 에이전트 실측 중 (Medium) 1.0 ~ 1.5 MD 4개 에이전트(Claude/Grok/Codex/OpenCode) 실기동 E2E 검증, Mock 단위 테스트
총계 중 (Medium) 3.0 ~ 4.5 Engineer-Days 안전하고 통제된 점진적 구현 가능

4. 상세 기술 아키텍처 및 구현 사양 (Rev. 2 보강)

4.1 결함 ① 해결책: 레지스트리 Fail-Closed 및 CAS 엔진 연동

  1. 표준 CAS 전이(deadline_timeout) 활용:
    • 임의의 신규 CLI 플래그(--expect-status)나 운영자 강제 오버라이드를 발명하지 않고, 이미 검증된 lib_py/delegation/engine.py:50의 deadline_timeout 전이를 호출합니다.
    • deadline_timeout은 잡이 pending 또는 running일 때만 원자적으로 error로 전이하며, 구독자 타임아웃 직후 백엔드에서 늦게 도착한 completed 이벤트가 이미 커밋된 경우 "conflicting error after terminal completed; refusing"으로 안전하게 거부(No-op)합니다.
    • expected_dispatch_token을 전달하여 이전 시도의 늦은 타임아웃이 재시도 잡을 오염시키는 ABA 레이스를 원천 차단합니다.
  2. multi-agent-mux-delegate-job 공통 종료 헬퍼 (finalize_unfinished) 도입:
    • direct 경로(:227) 및 loop/discuss 경로(:358, :375)에 공통 적용.
    • 구독자 종료 시 wait "$sub_pid" 수집 및 미완료 잡에 대해 finalize_unfinished를 호출하여 상태를 Fail-Closed 처리.
    • 래퍼 자체도 자식 구독자 프로세스를 안전하게 수거(kill "$sub_pid" 2>/dev/null; wait "$sub_pid")하여 3,600초 고아 프로세스 방지.
  3. 브로커 장애(rc=3)와 워커 실패 격리 (B-15 계약 준수):
    • 구독자가 rc=3(broker_unavailable)으로 종료되었으나 디스크 상에 이벤트가 살아있을 가능성이 있는 경우, 즉시 실패로 단정하지 않고 잡 데드라인까지 레지스트리를 재확인하여 브로커 일시 순단에 의한 거짓 실패를 방지합니다.
  4. 리테인 이벤트 오염 방지 (O-1):
    • loop/discuss 모드 실패 시 MQTT에 error 이벤트를 retain 상태로 발행하지 않아, 재시도 시 이전 에러 이벤트가 즉시 재수신되는 현상을 방지합니다.
  5. 오케스트레이터 러너 Fail-Fast 연결:
    • lib_py/loop/runner.py:delegate_job이 래퍼 실패(rc≠0) 시에도 registered job: 출력을 파싱하여 wait_for_job에 넘기도록 보장 (디스크가 이미 error이므로 1초 내 False 반환하여 65분 교착 원천 차단).

4.2 결함 ② 해결책: TUI 턴 핸드셰이크 시퀀스 및 예산 보강

  1. 핸드셰이크 실행 순서 (구독자 시작 전 배치):
    • 중요: wait_for_agent_idle 핸드셰이크는 반드시 구독자(job_subscriber.py) 기동 전에 완료되어야 합니다.
    • 구독자 실행 후에 대기할 경우, 에이전트의 유휴 대기 시간(15~30초)이 구독자의 120초 idle timeout 예산을 잠식(자가 포식)하는 결함이 발생합니다.
  2. 유휴 대기 예산 확대 (15초 → 120초):
    • Codex 등 복잡한 마크다운/도구 출력을 렌더링하는 대형 모델은 completed 이벤트 후 TUI가 완전히 done으로 복귀하는 데 실측 31초가 걸렸습니다.
    • 따라서 15초 하드코딩 대신 기본 120초(SKS_IDLE_TIMEOUT_MS=120000)를 적용하고 환경변수로 유연하게 조절할 수 있도록 합니다.
  3. Herdr 네이티브 대기 API 우선 사용 및 Fallback 견고화:
    • herdr agent wait <session> --until idle --until done --timeout "$SKS_IDLE_TIMEOUT_MS"를 우선 호출.
    • lib_py.loop.watcher.wait_once 메커니즘을 재사용하여 쉘 중복 구현을 방지.
    • 헤드리스 환경 등 agent_status가 unknown인 환경에서는 기존의 _pane_quiescent 정밀 휴리스틱으로 우아하게 Fallback.
  4. 수락 확인 및 이전 턴 잔여 토큰 오판 방지:
    • 키 전송 후 herdr agent prompt --wait --until working 수락 확인.
    • Fallback 경로에서 이전 턴의 잔여 busy 토큰(esc to interrupt 등)이 키 전송 전 캡처 화면에 이미 존재했던 경우, 이를 새 입력의 작업 시작 근거로 오판하지 않도록 사전 스냅샷과 비교 검증.

5. 단계별 실행 계획 (5-Phase Roadmap)

  • Phase 1: 레지스트리 CAS 표준 연동 및 delegate-job Fail-Closed 구현
    • engine.py deadline_timeout 호출 경로 확립
    • multi-agent-mux-delegate-job 내 direct(:227) 및 loop/discuss 종료 정리부 일원화
  • Phase 2: send_keys_safe 사전 유휴 핸드셰이크(Pre-Subscriber Idle Wait) 구현
    • SKS_IDLE_TIMEOUT_MS 기본 120초 지원
    • herdr agent wait 1차 게이트 및 잔여 토큰 오판 방지
  • Phase 3: 오케스트레이터 러너 Fail-Fast 방어막 보강
    • lib_py/loop/runner.py 예외 및 비정상 종료 시 조기 수거 연결
  • Phase 4: 단위/계약 격리 회귀 테스트 슈트 구축
    • tests/test_issue6_fail_closed.py
    • tests/test_issue6_turn_handshake.py
    • 기존 750개 pytest 100% 무결성 검증
  • Phase 5: 4개 실기동 에이전트(Claude, Grok, Codex, OpenCode) 대상 E2E 검증
    • 타임아웃 주입 및 TUI 연속 프롬프트 주입 실측 통과 확인

6. 결론

Issue #6은 코드베이스의 구조적 결함이라기보다, **비동기 이벤트 분산 환경에서 발생하는 상태 불일치(Event vs TUI)와 비정상 종료 시의 예외 처리 누락(Fail-Silent)**이 맞물려 발생한 전형적인 분산 시스템 레이스 컨디션입니다.

모든 리뷰어가 진단과 해결책에 완전히 합의하였고, 2차 라운드를 통해 엣지케이스까지 방어 사양이 완벽히 수립되었으므로 본 Rev.2 사양에 따라 즉시 구현에 착수할 수 있습니다.