리뷰 루프가 diff 131,072 바이트 초과 시 Errno 7 로 죽고 종료 코드 0 으로 끝난다 #36

Closed
opened 2026-09-22 21:34:04 +00:00 by Godopu · 1 comment
Owner

증상

리뷰 루프가 리뷰 위임 직전에 죽는다. 그런데 종료 코드는 0 이라 정상 완료처럼 보인다.

  File ".../subprocess.py", line 1990, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
OSError: [Errno 7] Argument list too long: 'bash'

로그의 마지막 줄은 Requesting code review from Reviewer '<name>'... 이고 그 뒤에 아무 오류 표시가 없다. 루프가 여러 회차를 도는 경우 1회차는 멀쩡히 끝나고 creator 가 수정 커밋을 올린 뒤 2회차에서 조용히 멈춘다 — diff 가 그 사이에 커졌기 때문이다.

원인

lib_py/loop/runner.py 가 수집한 diff 를 리뷰 프롬프트 문자열에 그대로 넣고(r_prompt = f"... 변경분:\n{changes_diff}"), 그 프롬프트를 bash 에 단일 argv 원소로 넘긴다. 리눅스의 단일 인자 상한 MAX_ARG_STRLEN 은 128 KiB = 131,072 바이트로 고정이며 ARG_MAX(여기서는 2,097,152) 와 별개다.

수집기 diff_collect.sh 는 자체 상한을 가지고 있지만 그 값이 커널 상한보다 높다.

MAM_DIFF_MAX_BYTES="${MAM_DIFF_MAX_BYTES:-200000}"
MAM_DIFF_MAX_LINES="${MAM_DIFF_MAX_LINES:-4000}"

그래서 131,072 에서 200,000 사이의 diff 는 수집기의 잘림 검사를 통과한 뒤 위임 단계에서 죽는다. 이 구간이 사각지대다.

재현

2026-09-22, 브랜치 fix/issue-23-24, 리뷰어 5명 --review-only 루프.

범위 git diff | wc -c 결과
1e78dae..4805fd4 115,643 정상 위임, 5명 판정 수신
1e78dae..087eaca 133,235 OSError: [Errno 7], 루프 중단, 종료 코드 0
getconf ARG_MAX        # 2097152 — 이 값이 아니다
# MAX_ARG_STRLEN = 32 * PAGE_SIZE = 131072, 커널 상수

영향

  • 커밋 10여 개 규모의 브랜치는 리뷰 루프로 리뷰할 수 없다. 이 저장소에서 실제로 도달한 크기다.
  • 실패가 성공처럼 보인다. 종료 코드 0 에 오류 요약도 없어서, 리뷰 판정이 하나도 수집되지 않았는데 루프가 끝난 것으로 오인하기 쉽다.
  • 같은 경로를 쓰는 self-review 와 원격 diff-only 리뷰도 같은 상한에 걸린다.

수정 방향

  1. 프롬프트를 argv 로 넘기지 않는다. stdin 이나 임시 파일로 전달한다. 상한 자체가 사라지므로 이게 근본 수정이다.
  2. 그게 어렵다면 최소한 수집기 상한을 커널 상한 아래로 내리고(MAM_DIFF_MAX_BYTES 기본값을 131072 미만으로), 잘림 표시가 붙은 diff 로 리뷰를 위임할지 거부할지 명시적으로 결정한다. 다만 잘린 diff 는 증거로서 약하므로 1번이 낫다.
  3. 어느 쪽이든 이 실패가 종료 코드 0 으로 끝나지 않게 한다. 위임이 실패했는데 루프가 성공처럼 끝나는 것이 가장 위험한 부분이다.

우회

리뷰 범위를 델타로 줄인다. --base <직전에 리뷰된 head> 로 브랜치 전체가 아니라 그 이후 변경만 리뷰한다. PR #35 의 4·5차 리뷰를 이 방법으로 수행했다.

## 증상 리뷰 루프가 리뷰 위임 직전에 죽는다. 그런데 종료 코드는 0 이라 정상 완료처럼 보인다. ``` File ".../subprocess.py", line 1990, in _execute_child raise child_exception_type(errno_num, err_msg, err_filename) OSError: [Errno 7] Argument list too long: 'bash' ``` 로그의 마지막 줄은 `Requesting code review from Reviewer '<name>'...` 이고 그 뒤에 아무 오류 표시가 없다. 루프가 여러 회차를 도는 경우 1회차는 멀쩡히 끝나고 creator 가 수정 커밋을 올린 뒤 2회차에서 조용히 멈춘다 — diff 가 그 사이에 커졌기 때문이다. ## 원인 `lib_py/loop/runner.py` 가 수집한 diff 를 리뷰 프롬프트 문자열에 그대로 넣고(`r_prompt = f"... 변경분:\n{changes_diff}"`), 그 프롬프트를 `bash` 에 **단일 argv 원소**로 넘긴다. 리눅스의 단일 인자 상한 `MAX_ARG_STRLEN` 은 128 KiB = 131,072 바이트로 고정이며 `ARG_MAX`(여기서는 2,097,152) 와 별개다. 수집기 `diff_collect.sh` 는 자체 상한을 가지고 있지만 그 값이 커널 상한보다 높다. ``` MAM_DIFF_MAX_BYTES="${MAM_DIFF_MAX_BYTES:-200000}" MAM_DIFF_MAX_LINES="${MAM_DIFF_MAX_LINES:-4000}" ``` 그래서 131,072 에서 200,000 사이의 diff 는 수집기의 잘림 검사를 통과한 뒤 위임 단계에서 죽는다. 이 구간이 사각지대다. ## 재현 2026-09-22, 브랜치 `fix/issue-23-24`, 리뷰어 5명 `--review-only` 루프. | 범위 | `git diff \| wc -c` | 결과 | |---|---:|---| | `1e78dae..4805fd4` | 115,643 | 정상 위임, 5명 판정 수신 | | `1e78dae..087eaca` | 133,235 | `OSError: [Errno 7]`, 루프 중단, 종료 코드 0 | ```sh getconf ARG_MAX # 2097152 — 이 값이 아니다 # MAX_ARG_STRLEN = 32 * PAGE_SIZE = 131072, 커널 상수 ``` ## 영향 - 커밋 10여 개 규모의 브랜치는 리뷰 루프로 리뷰할 수 없다. 이 저장소에서 실제로 도달한 크기다. - 실패가 성공처럼 보인다. 종료 코드 0 에 오류 요약도 없어서, 리뷰 판정이 하나도 수집되지 않았는데 루프가 끝난 것으로 오인하기 쉽다. - 같은 경로를 쓰는 self-review 와 원격 diff-only 리뷰도 같은 상한에 걸린다. ## 수정 방향 1. **프롬프트를 argv 로 넘기지 않는다.** stdin 이나 임시 파일로 전달한다. 상한 자체가 사라지므로 이게 근본 수정이다. 2. 그게 어렵다면 최소한 **수집기 상한을 커널 상한 아래로** 내리고(`MAM_DIFF_MAX_BYTES` 기본값을 131072 미만으로), 잘림 표시가 붙은 diff 로 리뷰를 위임할지 거부할지 명시적으로 결정한다. 다만 잘린 diff 는 증거로서 약하므로 1번이 낫다. 3. 어느 쪽이든 **이 실패가 종료 코드 0 으로 끝나지 않게** 한다. 위임이 실패했는데 루프가 성공처럼 끝나는 것이 가장 위험한 부분이다. ## 우회 리뷰 범위를 델타로 줄인다. `--base <직전에 리뷰된 head>` 로 브랜치 전체가 아니라 그 이후 변경만 리뷰한다. PR #35 의 4·5차 리뷰를 이 방법으로 수행했다.
Author
Owner

PR #37 로 해결했다. 브랜치 fix/issue-36, head 59be1c8.

원인. 러너가 수집한 diff 를 리뷰 프롬프트에 그대로 넣고, 그 프롬프트를 bash 에 단일 argv 원소로 넘겼다. 리눅스의 단일 인자 상한은 131,072 바이트인데 수집기 상한은 200,000 바이트라, 그 사이 크기가 걸러지지 않은 채 exec 단계에서 죽었다.

수정. 프롬프트가 지나는 경로 전체에서 긴 본문을 argv 로 넘기지 않는다. 파이썬 러너, delegate-job 래퍼, 등록부, 옛 셸 러너(MAM_LOOP_PY=0), lib.sh 제출 도우미가 모두 --prompt-file 로 넘긴다. 에이전트 pane 에는 brief 파일 경로만 담은 짧은 안내문이 주입된다. create_session 도 이제 같은 방식이다. 기존 --prompt 는 그대로 받는다.

종료 코드. 래퍼를 실행할 수 없거나 잡 등록이 실패하면 루프는 원인을 남기고 종료 코드 1 로 끝난다. 판정을 하나도 수집하지 못한 루프가 성공처럼 보이는 일은 없다. 이번 작업의 델타 리뷰 라운드에서 codex 가 NOT PASS 를 냈을 때, 루프가 실제로 종료 코드 1 로 끝나는 것을 확인했다.

수집기 상한. MAM_DIFF_MAX_BYTES 는 이제 리뷰어에게 보여줄 증거의 크기만 제한한다.

검증

항목 결과
전체 스위트 59be1c8 1111 통과 / 12 실패 / 19 건너뜀
리뷰어 5명 전원 PASS
이슈 테스트 22 통과

실패 12건은 이 호스트의 기존 기준선 실패다. 새 테스트는 131,072 바이트를 넘는 실제 프롬프트로 실제 exec 경로를 통과시키며, 수정 전 코드에서는 13건 중 11건이 실패한다.

리뷰 도중 codex 가 strace 로 실제 경로를 추적해, create_session 이 주입 안내문에 본문을 다시 붙여 execve 가 E2BIG 로 실패하는 경로를 찾아냈다. 이 지적까지 고친 뒤 codex 가 제출한 재현 자산을 수정 없이 돌려 통과를 확인했다.

PR #37 로 해결했다. 브랜치 `fix/issue-36`, head `59be1c8`. **원인.** 러너가 수집한 diff 를 리뷰 프롬프트에 그대로 넣고, 그 프롬프트를 `bash` 에 단일 argv 원소로 넘겼다. 리눅스의 단일 인자 상한은 131,072 바이트인데 수집기 상한은 200,000 바이트라, 그 사이 크기가 걸러지지 않은 채 exec 단계에서 죽었다. **수정.** 프롬프트가 지나는 경로 전체에서 긴 본문을 argv 로 넘기지 않는다. 파이썬 러너, delegate-job 래퍼, 등록부, 옛 셸 러너(`MAM_LOOP_PY=0`), lib.sh 제출 도우미가 모두 `--prompt-file` 로 넘긴다. 에이전트 pane 에는 brief 파일 경로만 담은 짧은 안내문이 주입된다. create_session 도 이제 같은 방식이다. 기존 `--prompt` 는 그대로 받는다. **종료 코드.** 래퍼를 실행할 수 없거나 잡 등록이 실패하면 루프는 원인을 남기고 종료 코드 1 로 끝난다. 판정을 하나도 수집하지 못한 루프가 성공처럼 보이는 일은 없다. 이번 작업의 델타 리뷰 라운드에서 codex 가 NOT PASS 를 냈을 때, 루프가 실제로 종료 코드 1 로 끝나는 것을 확인했다. **수집기 상한.** `MAM_DIFF_MAX_BYTES` 는 이제 리뷰어에게 보여줄 증거의 크기만 제한한다. ## 검증 | 항목 | 결과 | |---|---| | 전체 스위트 `59be1c8` | 1111 통과 / 12 실패 / 19 건너뜀 | | 리뷰어 5명 | 전원 PASS | | 이슈 테스트 | 22 통과 | 실패 12건은 이 호스트의 기존 기준선 실패다. 새 테스트는 131,072 바이트를 넘는 실제 프롬프트로 실제 exec 경로를 통과시키며, 수정 전 코드에서는 13건 중 11건이 실패한다. 리뷰 도중 codex 가 `strace` 로 실제 경로를 추적해, create_session 이 주입 안내문에 본문을 다시 붙여 `execve` 가 `E2BIG` 로 실패하는 경로를 찾아냈다. 이 지적까지 고친 뒤 codex 가 제출한 재현 자산을 수정 없이 돌려 통과를 확인했다.
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#36