11 KiB
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)
- 실측 및 재현 결과:
- 이슈 지목 위치(
:358-362)의 진위:_phase == "worker"에서job_status != "completed"일 때break후 레지스트리(registry.py status --set error) 갱신 없이 함수 끝에서 로그 경로를 출력하며exit 0으로 종료되는 현상이 정확히 확인되었습니다.- 동일한 결함이 리뷰어 단계(
:375-378) 조기 탈출 분기에도 동일하게 존재합니다.
- 오케스트레이터 실사용 경로(
--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)**하고 있었습니다.
- 상위 오케스트레이터(
- 파급 효과 (65분 Hang):
- 상위 루프의
wait_for_job은completed,error,cancelled3개 터미널 상태만 종료 조건으로 인식합니다. - 구독자가 120초 idle timeout(
sub_rc=2)으로 죽었음에도 레지스트리가pending또는running으로 방치되어 상위 루프가 기본값인 3,900초(65분) 동안 블로킹됩니다. - 실제 운영 레지스트리 포렌식 결과
93a44347,2961b822,ee2426713건의 실운영 작업이 정확히 이 결함으로 장시간 교착 상태에 방치되었던 사실이 실증되었습니다.
- 상위 루프의
- 이슈 지목 위치(
2.2 결함 ②: 에이전트 이벤트 발행과 TUI 복귀 간의 레이스 컨디션
- 판정: 실재하는 설계 결함 (CONFIRMED REAL, P1)
- 실측 및 재현 결과:
- 비동기 상태 불일치 레이스:
- 에이전트 프로세스가 MQTT로
completed이벤트를 발행한 직후에도 터미널 화면은 마크다운 렌더링, 버퍼 플러시, 툴 호출 정리 등으로 인해 여전히working/busy상태일 수 있습니다. - 실측 결과 Codex 등 대형 모델의 경우
completed발행 후 TUI가done으로 떨어지는 데 최대 31초가 소요되기도 했습니다.
- 에이전트 프로세스가 MQTT로
- 프롬프트 묵살 및 유실:
- 다음 단계(리뷰어 등)가 즉시 프롬프트를 전송하면 TUI가 입력을 무시하거나 첫 글자/단어가 잘린 채 주입되어 문맥이 깨진 채 실행되거나 무응답 상태에 빠집니다.
- 두 결함 간의 악순환 증폭:
- 결함 ②(TUI 미준비 프롬프트 유실) 발생 → 워커 무응답 → 구독자 120초 타임아웃(
sub_rc=2) → 결함 ①(Fail-Silent) 발동 → 레지스트리pending/running방치 → 상위 루프 65분 영구 교착(Hang).
- 결함 ②(TUI 미준비 프롬프트 유실) 발생 → 워커 무응답 → 구독자 120초 타임아웃(
- 비동기 상태 불일치 레이스:
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 엔진 연동
- 표준 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 레이스를 원천 차단합니다.
- 임의의 신규 CLI 플래그(
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초 고아 프로세스 방지.
- direct 경로(
- 브로커 장애(
rc=3)와 워커 실패 격리 (B-15 계약 준수):- 구독자가
rc=3(broker_unavailable)으로 종료되었으나 디스크 상에 이벤트가 살아있을 가능성이 있는 경우, 즉시 실패로 단정하지 않고 잡 데드라인까지 레지스트리를 재확인하여 브로커 일시 순단에 의한 거짓 실패를 방지합니다.
- 구독자가
- 리테인 이벤트 오염 방지 (O-1):
- loop/discuss 모드 실패 시 MQTT에
error이벤트를 retain 상태로 발행하지 않아, 재시도 시 이전 에러 이벤트가 즉시 재수신되는 현상을 방지합니다.
- loop/discuss 모드 실패 시 MQTT에
- 오케스트레이터 러너 Fail-Fast 연결:
lib_py/loop/runner.py:delegate_job이 래퍼 실패(rc≠0) 시에도registered job:출력을 파싱하여wait_for_job에 넘기도록 보장 (디스크가 이미error이므로 1초 내 False 반환하여 65분 교착 원천 차단).
4.2 결함 ② 해결책: TUI 턴 핸드셰이크 시퀀스 및 예산 보강
- 핸드셰이크 실행 순서 (구독자 시작 전 배치):
- 중요:
wait_for_agent_idle핸드셰이크는 반드시 구독자(job_subscriber.py) 기동 전에 완료되어야 합니다. - 구독자 실행 후에 대기할 경우, 에이전트의 유휴 대기 시간(15~30초)이 구독자의 120초 idle timeout 예산을 잠식(자가 포식)하는 결함이 발생합니다.
- 중요:
- 유휴 대기 예산 확대 (15초 → 120초):
- Codex 등 복잡한 마크다운/도구 출력을 렌더링하는 대형 모델은
completed이벤트 후 TUI가 완전히done으로 복귀하는 데 실측 31초가 걸렸습니다. - 따라서 15초 하드코딩 대신 기본 120초(
SKS_IDLE_TIMEOUT_MS=120000)를 적용하고 환경변수로 유연하게 조절할 수 있도록 합니다.
- Codex 등 복잡한 마크다운/도구 출력을 렌더링하는 대형 모델은
- 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.
- 수락 확인 및 이전 턴 잔여 토큰 오판 방지:
- 키 전송 후
herdr agent prompt --wait --until working수락 확인. - Fallback 경로에서 이전 턴의 잔여 busy 토큰(
esc to interrupt등)이 키 전송 전 캡처 화면에 이미 존재했던 경우, 이를 새 입력의 작업 시작 근거로 오판하지 않도록 사전 스냅샷과 비교 검증.
- 키 전송 후
5. 단계별 실행 계획 (5-Phase Roadmap)
- Phase 1: 레지스트리 CAS 표준 연동 및 delegate-job Fail-Closed 구현
engine.pydeadline_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 wait1차 게이트 및 잔여 토큰 오판 방지
- Phase 3: 오케스트레이터 러너 Fail-Fast 방어막 보강
lib_py/loop/runner.py예외 및 비정상 종료 시 조기 수거 연결
- Phase 4: 단위/계약 격리 회귀 테스트 슈트 구축
tests/test_issue6_fail_closed.pytests/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 사양에 따라 즉시 구현에 착수할 수 있습니다.