9.8 KiB
9.8 KiB
[reviewer-opencode-01] Issue #6 정밀 검토 및 해결 방안 보고서
- Job:
17f2e688/ 검토자:reviewer-opencode-01(opencode) - 검토 방법: 이슈가 인용한 모든 위치를 라이브 소스에서 직접 읽고, 결함 체인 전체(worker break → 레지스트리 → wait_for_job)를 코드 라인 단위로 추적하며, 셸 루프(
run_loop.sh)와 Python 루프(lib_py/loop/runner.py) 양쪽 소비자를 대조하고, 기존 테스트 커버리지를 확인했다. 요약을 신뢰하지 않고 재현 경로를 직접 검증했다.
1. 버그 진위 여부 검토 결과
결함 ① (Fail-Silent on Worker Failure) — 실재 확인 (CONFIRMED REAL)
이슈의 인용은 정확하다. multi-agent-mux-delegate-job의 반복 루프:
- :351-361 —
_phase == "worker"에서job_status != "completed"인 경우(주입 실패로 인한 subscriber timeout, 프롬프트 미수신 idle timeout 등)echo ... Terminating workflow.후break. - 이
break경로에는 어디에도registry.py status --set호출이 없다. 반비교: 루프 내 다른 종료 경로는 레지스트리를 올바르게 갱신한다 — reviewer 승인 시--set "completed"(:394), 최대 반복 도달 시--set "error"(:400). - 즉 worker 실패 조기 탈출이 유일하게 레지스트리를 방치하는 종료 경로다. 잡은 생성 시
status: "pending"(lib_py/delegation/registry.py:84)으로 등록되므로, 이 경로를 밟으면<job_id>.json은 **영구히pending**으로 남는다. - 함수는 이후 validation hook을 돌고 audit-log 경로를 stdout으로 출력한 뒤 정상 종료한다 —
exit 0(fail-silent). 이슈 기술과 정확히 일치.
소비자 측 교착 확인: 셸 wait_for_job(run_loop.sh:270-306)과 Python wait_for_job(lib_py/loop/runner.py:291-323) 모두 종료 조건이 completed(→rc 0), error/cancelled(→rc 1)뿐이고, 그 외 상태(pending 포함)는 sleep $check_interval 후 재폴링한다. pending은 어떤 종료 조건에도 해당하지 않으므로 max_wait=3900s를 전량 소진한 뒤 타임아웃으로 rc=1 — 정확히 이슈가 기술한 65분 영구 대기. 두 런타임(v4.4.0의 Python 코어 포함) 모두 동일하게 노출된다.
결함 ② (Race Condition on TUI Turn Handshake) — 실재 확인 (CONFIRMED REAL, 그러나 이미 부분 완화 장치 존재 — 임계값/대기시간 문제)
send_keys_safe(lib.sh:2231-)는 주입 전_pane_quiescent(:2189)로 연속 2회 동일 캡처(렌더러 정지)를 확인한다. 이것이 바로 이슈가 요구하는 settling 브리미티브의 존재 자체는 이미 구현되어 있다.- 다만 실측 파라미터:
SKS_QUIESCENT_TRIES=20×SKS_QUIESCENT_INTERVAL=0.5s= 최대 10초 대기. 에이전트가 도구로completed이벤트를 발행한 직후(턴 내부) 오케스트레이터가 즉시 다음 잡을 생성해 주입하면, 에이전트 TUI는 아직 턴 출력을 렌더링 중이므로 캡처 내용이 계속 변한다. 렌더링이 10초 이상 지속되면_pane_quiescent가 rc=1을 반환하고send_keys_safe는 "pane never quiesced"와 함께 주입 자체를 거부한다. - 결함 ②와 ①의 연쇄(핵심):
run_agent의 주입 실패는 rc=1을 반환하지만, 루프의 호출부(:327)run_agent "$JOB_ID" ...는 반환값을 무시한다(if ! run_agent아님). 이후wait "$sub_pid"는 프롬프트가 주입되지 않은 에이전트를 대기하다 idle timeout으로 rc≥2 →job_status="timeout"→ worker break → 결함 ①의 fail-silent →pending교착. 즉 ②는 ①의 주요 트리거이며, ①은 ②의 피해를 교착으로 증폭한다. 두 결함은 독립 패치가 아니라 하나의 수정 단위로 다뤄야 한다.
2. 결함 메커니즘 및 파급 효과 분석
| 항목 | 분석 |
|---|---|
| 발생 빈도 | 결함 ②는 렌더링 지연 > 10s인 에이전트/대형 출력에서 현실적으로 발생(이슈 자체가 실관측 사례). ②가 터질 때마다 ①로 인해 65분 교착이 뒤따른다. |
| 루프 관점 파급 | run_loop.sh의 --max-loop 사이클 전체가 단일 worker 실패 하나로 최대 65분 정지. 병렬 리뷰어(--all-reviewer) 펜딘트 시 리소스 점유 + 대기 누적. |
| 데이터 무결성 | <job_id>.json의 pending은 "미수행"과 "수행 실패"를 구별 불가하게 만든다 — 감사 로그(events.log)에 subscriber 기록은 남지만, 상태 원장 자체가 거짓("아직 안 함"). 이는 B-14(로컬 디스크 100% 동기화)에서 수립한 "디스크 상태가 진실"이라는 계약의 잔여 예외다. |
| 심각도 | ①: HIGH (영구 교착 + 원장 오염). ②: MEDIUM-HIGH (교착의 주 트리거, 다만 quiescent 브리미티브가 있어 완전 무방비는 아님). |
3. 수정 난이도 및 위험도 평가
| 결함 | 난이도 | 공수 | 파급 범위 / 위험 |
|---|---|---|---|
| ① 레지스트리 fail-closed 전이 | 하 (Low) | ~0.5일 (테스트 포함) | worker/reviewer 양 break 경로에 status --set error 1줄씩 추가. 소비자(wait_for_job 2개)는 이미 error를 정상 종료 조건으로 다루므로 소비자 변경 불필요 — 파급 최소. 유일 위험: break 시점의 상태를 error로 덮어쓸 때 이미 디스크에 terminal 상태가 있는 경우(rc=3 broker_unavailable 분기가 이미 디스크 우선 병합을 하므로 그대로 재사용). |
| ② TUI settling 안전화 | 중 (Medium) | ~1일 | run_agent 반환값 접수(루프 :327의 if ! run_agent 화) + quiescent 파라미터 튜닝/환경변수화(SKS_QUIESCENT_TRIES/INTERVAL는 이미 튜너블). 주입 실패를 즉시 error 전이로 연결하면 ②가 터려도 ① 덕분에 루프가 빠르게 실패 종료된다(사실상 ②의 폭발 반경을 ① 패치가 절단). 위험: quiescent 대기 연장은 루프 지연을 늘리므로 상한 환경변수로 봉인 필요. |
4. 구체적 해결 방안 및 아키텍처 개선안
결함 ① — fail-closed 레지스트리 전이
- worker break (:360) 직전:
"$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" status --job "$JOB_ID" --set "error"추가 —job_status변수가 이미 있으므로, 보존을 원하면--set "$job_status"계열로 세분화해도 되나wait_for_job의 종료 어휘가error이므로 정규화해서error로 기록 권장. - reviewer break (:365) 동일 적용 (대칭 결함, 이슈에는 미기재지만 동일 라인 구조).
run_agent실패 즉시 전이(② 연쇄 절단): 루프 :327을if ! run_agent ...; then <set error>; break; fi로 변경 — 주입 실패가 subscriber 120초 대기를 거치지 않고 즉시 실패로 확정.- 중복 방지: rc=3 broker_unavailable 분기(:342-347)의 디스크 우선 병합 결과(
completed/error)를 그대로 존중 — 이미 terminal이면 덮어쓰지 않는 가드(disk_st체크 재사용).
결함 ② — TUI settling 안전 처리
- 즉시 조치: 주입 실패의 즉시 error 전이(위 4-3)가 핵심 — settling은 완화 장치일 뿐, 실패 시의 원장 정직성이 본질.
- quiescent 파라미터 환경변수화 튜닝:
SKS_QUIESCENT_TRIES(기본 20)를 잡 주입 맥락에서MAM_SKS_INJECT_TRIES(예: 40회×0.5s=20s)로 상향하는 옵션 + 프롬프트 복귀(>) 감지 시 조기 탈출 —_pane_quiescent가 "동일 캡처"만 보는 것을, adapter의input_prompt/input_placeholder사실(lib_py.agents facts)과 교차 확인하면 idle 복귀를 정확히 포착(사실 브리지가 이미 내보내는MAM_INPUT_PROMPT/MAM_INPUT_PLACEHOLDER/MAM_INPUT_RULE_PATTERN을send_keys_safe가 이미 수신하므로, quiescent 성공 판정에 placeholder 프레즌스 조건을 추가하는 것이 최소 변경). - 선택(중기): 주입 재시도 1회 — 실패 시 settle 재대기 후 1회 재주입, 재실패 시 error 전이. 무한 재시도는 금지(교착의 새 형태).
5. 검증 및 테스트 계획
- 결함 ① 회귀 가드 (신규): mock herdr/agent 환경에서 worker 주입 실패를 유도(
run_agent가 rc=1 반환하도록) → 잡.json이error로 갱신되고wait_for_job이max_wait소진 없이 rc=1로 즉시 복귀함을 단언. 반례: broker_unavailable+디스크 completed 시 덮어쓰기 없음. - 결함 ② 회귀 가드 (신규):
_pane_quiescent가 연속 변화 캡처(렌더링 중)를 시뮬레이션하는 mock으로send_keys_safe의 거부 →run_agentrc=1 → 레지스트리error전이 연쇄를 단언. - 기존 스위트:
tests/test_o2_race_free_lock.py(loop lock), Issue #5 Phase 2a/2b G-가드들, B-14/B-15의 G-1~G-10(tests/test_b13*,test_issue5_*)이 이번 변경과 충돌 없음을 확인 — 특히test_g5_no_blocking_io_while_the_registry_lock_is_held가 새status --set호출 위치(락 밖)와 정합. - 전체 스위트: 현재 기준선(750 passed) 유지 확인.
- 라이브 스모크: 실제 2-에이전트 위임에서 일부러 주입을 방해(세션 일시 정지) → 루프가 65분이 아니라 수십 초 내에
error로 종료되는지 실측.
총평
두 결함 모두 실재하며 상호 증폭 관계다(②가 트리거, ①이 교착으로 변환). ①은 원장 정직성 계약(B-14 계열)의 마지막 잔여 구멍이고 수정 비용이 매우 낮다(난이도 하, ~0.5일). ②는 이미 존재하는 quiescent 프리미티브를 주입 맥락에 맞게 튜닝하고 run_agent 반환값을 루프가 수용하도록 하는 중간 난이도 작업(~1일)이다. 둘을 한 수정 단위로 묶어 진행할 것을 권고하며, 설계 변경 수준의 재계획은 불필요하다.