docs(issue-6): establish phase 1-5 implementation plan and task checklist (Rev.2)

This commit is contained in:
Godopu committed 2026-09-08 10:39:54 +09:00
1 parent f98b9582c7
commit 7b761be7b8
6 files changed
+827

No files matched your search

@@ -0,0 +1,121 @@
# 📑 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](https://git.godopu.com/tmpl/multi-agent-mux/issues/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 사양에 따라 즉시 구현에 착수할 수 있습니다.