2026-09-21 #25 작업 루프에서, 정상 동작 중이던 creator job 이 등록 후 정확히 3600초에 닫혔다.
시각
사건
02:50:46
job 98c23c75 registered
02:50:52
started
02:51:23 ~ 03:50:23
progress heartbeat 119회 연속, 미스 0회
03:50:46
status_changed → error (등록 + 정확히 3600 s)
03:50:47
error 발행, detail deadline_timeout
에이전트는 살아서 작업 중이었고(그 시점에도 herdr working, 백그라운드 셸 4개), 작업물도 남아 있었다(워크트리에 커밋 4개). 이후 agent 가 발행한 completed 는 "conflicting completed after terminal error" 로 거부됐다.
원인은 무응답이 아니라 벽시계 예산 초과다. 검증 계획이 전체 스위트를 브랜치·base 두 번 돌리도록 했고, 이 호스트에서 한 번이 약 28분이라 56분 + 구현 시간이 1시간을 넘겼다.
원인
루프 러너는 delegate-job 래퍼에 --timeout 을 전달하지 않는다. lib_py/loop/runner.py 의 delegate_job 이 넘기는 플래그는 --agent-session·--agent·--type·--role·--registry-dir·--prompt 뿐이다. heartbeat 계열만 env 로 전달한다(runner.py:385-390: MAM_HEARTBEAT_SEC·MAM_HEARTBEAT_MISSES·MAM_ACK_TIMEOUT_S).
래퍼의 기본값은 상수다: multi-agent-mux-delegate-job:135TIMEOUT=3600. 환경변수 fallback 이 없어 --timeout 플래그로만 바꿀 수 있는데, 루프 사용자에게는 그 경로가 없다.
따라서 루프로 위임하는 모든 job 은 1시간 고정이다.
영향
전체 스위트처럼 긴 검증이 필요한 작업은 구조적으로 1시간 안에 끝나야 한다. 넘기면 정상 작업이 실패로 기록되고 루프가 종료된다.
이벤트 detail 이 deadline_timeout 이라, 로그만 보면 "무응답"(K×N 초 이벤트 없음)과 구분되지 않는다. 이번 사례는 heartbeat 가 마지막까지 도착했는데도 같은 라벨이 붙었다.
작업물 자체는 디스크에 남지만, 에이전트의 completed 가 거부되어 job 기록이 결과와 어긋난다.
제안
래퍼의 TIMEOUT 에 env fallback 을 준다(예: MAM_JOB_TIMEOUT_S). heartbeat 계열과 같은 방식이라 일관된다.
루프가 그 값을 전달하고, 필요하면 루프 옵션(--job-timeout <sec>)으로도 노출한다.
예산 초과와 무응답을 이벤트 detail 에서 구분한다(예: wall_clock_timeout vs deadline_timeout).
문서화: 루프 SKILL.md 의 생존 계약 절에 "위임 job 은 기본 1시간"을 명시하고, 긴 검증은 작업을 나누거나 예산을 올리도록 안내한다.
main 6fc9194 (v4.7.0), Linux, Claude Code v2.1.270
## 증상
2026-09-21 #25 작업 루프에서, 정상 동작 중이던 creator job 이 등록 후 정확히 **3600초**에 닫혔다.
| 시각 | 사건 |
|---|---|
| 02:50:46 | job `98c23c75` registered |
| 02:50:52 | `started` |
| 02:51:23 ~ 03:50:23 | `progress` heartbeat **119회 연속, 미스 0회** |
| 03:50:46 | `status_changed → error` (등록 + 정확히 3600 s) |
| 03:50:47 | `error` 발행, detail `deadline_timeout` |
에이전트는 살아서 작업 중이었고(그 시점에도 herdr `working`, 백그라운드 셸 4개), 작업물도 남아 있었다(워크트리에 커밋 4개). 이후 agent 가 발행한 `completed` 는 "conflicting completed after terminal error" 로 거부됐다.
원인은 무응답이 아니라 **벽시계 예산 초과**다. 검증 계획이 전체 스위트를 브랜치·base 두 번 돌리도록 했고, 이 호스트에서 한 번이 약 28분이라 56분 + 구현 시간이 1시간을 넘겼다.
## 원인
- 루프 러너는 delegate-job 래퍼에 `--timeout` 을 **전달하지 않는다**. `lib_py/loop/runner.py` 의 `delegate_job` 이 넘기는 플래그는 `--agent-session`·`--agent`·`--type`·`--role`·`--registry-dir`·`--prompt` 뿐이다. heartbeat 계열만 env 로 전달한다(`runner.py:385-390`: `MAM_HEARTBEAT_SEC`·`MAM_HEARTBEAT_MISSES`·`MAM_ACK_TIMEOUT_S`).
- 래퍼의 기본값은 상수다: `multi-agent-mux-delegate-job:135` `TIMEOUT=3600`. 환경변수 fallback 이 없어 `--timeout` 플래그로만 바꿀 수 있는데, 루프 사용자에게는 그 경로가 없다.
- 따라서 **루프로 위임하는 모든 job 은 1시간 고정**이다.
## 영향
- 전체 스위트처럼 긴 검증이 필요한 작업은 구조적으로 1시간 안에 끝나야 한다. 넘기면 정상 작업이 실패로 기록되고 루프가 종료된다.
- 이벤트 detail 이 `deadline_timeout` 이라, 로그만 보면 "무응답"(K×N 초 이벤트 없음)과 구분되지 않는다. 이번 사례는 heartbeat 가 마지막까지 도착했는데도 같은 라벨이 붙었다.
- 작업물 자체는 디스크에 남지만, 에이전트의 `completed` 가 거부되어 job 기록이 결과와 어긋난다.
## 제안
1. 래퍼의 `TIMEOUT` 에 env fallback 을 준다(예: `MAM_JOB_TIMEOUT_S`). heartbeat 계열과 같은 방식이라 일관된다.
2. 루프가 그 값을 전달하고, 필요하면 루프 옵션(`--job-timeout <sec>`)으로도 노출한다.
3. 예산 초과와 무응답을 이벤트 detail 에서 구분한다(예: `wall_clock_timeout` vs `deadline_timeout`).
4. 문서화: 루프 SKILL.md 의 생존 계약 절에 "위임 job 은 기본 1시간"을 명시하고, 긴 검증은 작업을 나누거나 예산을 올리도록 안내한다.
## 관련
- #18 생존·전달 판정의 프로토콜 계약 (heartbeat·ack 계약의 출처)
- #17 운영 신뢰성 로드맵
- #30 살아 있지만 멈춘 에이전트 감지 (같은 계열의 판정 문제)
- 관측 출처: #25 작업 루프 (`loop-20260921T023718Z-027d67`)
## 환경 정보
- main `6fc9194` (v4.7.0), Linux, Claude Code v2.1.270
작업 시 감안할 점 — #17 항목 4 의 경계 기준과 처리 설계 (병합 워크스페이스, 2026-09-25)
경계 기준(#17 댓글 참조): 결정을 내리는 코드는 Python, 외부 프로세스를 만지는 코드는 bash. 벽시계·무응답 판정과 종결 라벨은 "결정" 이므로 Python 에 둔다. 지금은 두 시계를 Python 구독자(job_subscriber.py)가 세고 라벨은 bash 래퍼(finalize_unfinished)가 붙이는 갈라진 구조라, 항목 4 가 종결 처리를 Python 으로 옮긴 뒤 이 이슈를 그 위에 얹는 순서를 권한다. 항목 4 보다 먼저 해야 한다면 bash 를 최소로만 고치고 라벨 결정은 Python 쪽 반환값(구독자 rc 2 에 어느 시계였는지 부호)으로 넘긴다.
처리 설계 제안
벽시계 도달 시 래퍼는 잡을 바로 닫지 않고 멈춰서 묻는다: progress --detail budget_exhausted 발행 + 결정 파일(예: <jobs>/<jid>.extend) 대기(짧은 유예).
결정은 정책 또는 오케스트레이터가 한다. 루프가 돌 때는 루프 러너가 정책으로 답한다(예: 마지막 heartbeat 가 K×N 안이고 연장 횟수 < MAM_JOB_EXTEND_MAX(기본 1)이면 같은 예산만큼 1회 연장). 루프 밖 직접 위임이면 오케스트레이터가 결정 파일을 쓴다.
거부·무응답이면 error / wall_clock_timeout 으로 닫는다. 무응답(K×N 초 이벤트 없음)은 종전대로 deadline_timeout. 두 라벨을 분리해야 사후 추론(events.log 의 heartbeat 시각 비교) 없이 잡 레코드 한 줄로 구분된다.
연장 근거를 heartbeat 만으로 삼지 않는다. heartbeat 타이머는 프로세스 생존만 증명한다(codex 가 API 재접속에 3시간 갇힌 동안에도 정상 발행됨). #30 의 판정(state_change_seq 정체 등)을 연장 거부 근거로 쓴다.
예산 조절: 래퍼 TIMEOUT 에 MAM_JOB_TIMEOUT_S fallback, 루프 --job-timeout <sec> 전달. 기본 3600 은 유지(대부분의 잡에서 1시간 초과는 이상 신호). 연장 상한은 남긴다 — 무한 대기 방지 장치라는 뜻을 잃지 않도록.
하위 잡(#34, v4.7.4)의 표 분류는 라벨이 나뉘면 "미확인 종결(벽시계)" 과 "무응답" 을 따로 셀 수 있다.
참고: 예산은 잡 하나 단위다(루프 사이클 전체가 아님). 관측 사례는 creator 잡 하나가 전체 스위트를 두 번 돌려 혼자 1시간을 넘긴 것이다.
## 작업 시 감안할 점 — #17 항목 4 의 경계 기준과 처리 설계 (병합 워크스페이스, 2026-09-25)
**경계 기준(#17 댓글 참조)**: 결정을 내리는 코드는 Python, 외부 프로세스를 만지는 코드는 bash. 벽시계·무응답 판정과 종결 라벨은 "결정" 이므로 Python 에 둔다. 지금은 두 시계를 Python 구독자(`job_subscriber.py`)가 세고 라벨은 bash 래퍼(`finalize_unfinished`)가 붙이는 갈라진 구조라, 항목 4 가 종결 처리를 Python 으로 옮긴 뒤 이 이슈를 그 위에 얹는 순서를 권한다. 항목 4 보다 먼저 해야 한다면 bash 를 최소로만 고치고 라벨 결정은 Python 쪽 반환값(구독자 rc 2 에 어느 시계였는지 부호)으로 넘긴다.
**처리 설계 제안**
1. 벽시계 도달 시 래퍼는 잡을 바로 닫지 않고 멈춰서 묻는다: `progress --detail budget_exhausted` 발행 + 결정 파일(예: `<jobs>/<jid>.extend`) 대기(짧은 유예).
2. 결정은 정책 또는 오케스트레이터가 한다. 루프가 돌 때는 루프 러너가 정책으로 답한다(예: 마지막 heartbeat 가 K×N 안이고 연장 횟수 < `MAM_JOB_EXTEND_MAX`(기본 1)이면 같은 예산만큼 1회 연장). 루프 밖 직접 위임이면 오케스트레이터가 결정 파일을 쓴다.
3. 거부·무응답이면 `error / wall_clock_timeout` 으로 닫는다. 무응답(K×N 초 이벤트 없음)은 종전대로 `deadline_timeout`. 두 라벨을 분리해야 사후 추론(`events.log` 의 heartbeat 시각 비교) 없이 잡 레코드 한 줄로 구분된다.
4. 연장 근거를 heartbeat 만으로 삼지 않는다. heartbeat 타이머는 프로세스 생존만 증명한다(codex 가 API 재접속에 3시간 갇힌 동안에도 정상 발행됨). #30 의 판정(`state_change_seq` 정체 등)을 연장 거부 근거로 쓴다.
5. 예산 조절: 래퍼 `TIMEOUT` 에 `MAM_JOB_TIMEOUT_S` fallback, 루프 `--job-timeout <sec>` 전달. 기본 3600 은 유지(대부분의 잡에서 1시간 초과는 이상 신호). 연장 상한은 남긴다 — 무한 대기 방지 장치라는 뜻을 잃지 않도록.
6. 하위 잡(#34, v4.7.4)의 표 분류는 라벨이 나뉘면 "미확인 종결(벽시계)" 과 "무응답" 을 따로 셀 수 있다.
**참고**: 예산은 잡 하나 단위다(루프 사이클 전체가 아님). 관측 사례는 creator 잡 하나가 전체 스위트를 두 번 돌려 혼자 1시간을 넘긴 것이다.
설계 합의 (B): 네 리뷰어 모두 #39 분리에 동의(claude 조건부). 보완책(공통): 연장 횟수·누적 예산 상한은 오케스트레이터(소비자) 쪽에 두고, token/attempt 로 인증된 요청만 멱등 처리해 새 절대 deadline 을 확정·배포한다. heartbeat 만 있고 state_change_seq 가 그대로면 거절한다(grok/agy). 원격 잡에도 같은 규칙. #39 에서 설계할 때 이 조건을 출발점으로 삼습니다.
(D) 항목 4 로 옮길 bash 판정 목록(리뷰어 합의): FINALIZE_CLOCK 선택·디스크 유예/재관측 종료 결정, 감시 여부 판정(remote/human/MAM_STALL_WATCH), MAM_JOB_TIMEOUT_S 검증·기본값(선행 0 이 bash 에서 8진수로 읽히는 문제 포함), rc→close_req/라벨 매핑, _map_sub_rc·cmd_wait 정규화, start/stop_stall_watch 수명 관리. bash 에는 spawn/wait/kill·키 주입·얇은 CLI 호환만 남깁니다. 문서 후속: MAM_WAIT_GRACE_S 미문서, 러너의 잘못된 MAM_JOB_TIMEOUT_S 무언.
PR #41 병합(v4.8.0)으로 닫힙니다. 예산(`--timeout` > `MAM_JOB_TIMEOUT_S` > 3600, 루프 `--job-timeout`)과 라벨(구독자 rc 4 → `wall_clock_timeout`, rc 2 → `deadline_timeout`)은 합의대로이며, 예산 연장은 #39 로 분리했습니다.
설계 합의 (B): 네 리뷰어 모두 #39 분리에 동의(claude 조건부). 보완책(공통): 연장 횟수·누적 예산 상한은 오케스트레이터(소비자) 쪽에 두고, token/attempt 로 인증된 요청만 멱등 처리해 새 절대 deadline 을 확정·배포한다. heartbeat 만 있고 `state_change_seq` 가 그대로면 거절한다(grok/agy). 원격 잡에도 같은 규칙. #39 에서 설계할 때 이 조건을 출발점으로 삼습니다.
(D) 항목 4 로 옮길 bash 판정 목록(리뷰어 합의): `FINALIZE_CLOCK` 선택·디스크 유예/재관측 종료 결정, 감시 여부 판정(remote/human/`MAM_STALL_WATCH`), `MAM_JOB_TIMEOUT_S` 검증·기본값(선행 0 이 bash 에서 8진수로 읽히는 문제 포함), rc→close_req/라벨 매핑, `_map_sub_rc`·`cmd_wait` 정규화, `start/stop_stall_watch` 수명 관리. bash 에는 spawn/wait/kill·키 주입·얇은 CLI 호환만 남깁니다. 문서 후속: `MAM_WAIT_GRACE_S` 미문서, 러너의 잘못된 `MAM_JOB_TIMEOUT_S` 무언.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
증상
2026-09-21 #25 작업 루프에서, 정상 동작 중이던 creator job 이 등록 후 정확히 3600초에 닫혔다.
98c23c75registeredstartedprogressheartbeat 119회 연속, 미스 0회status_changed → error(등록 + 정확히 3600 s)error발행, detaildeadline_timeout에이전트는 살아서 작업 중이었고(그 시점에도 herdr
working, 백그라운드 셸 4개), 작업물도 남아 있었다(워크트리에 커밋 4개). 이후 agent 가 발행한completed는 "conflicting completed after terminal error" 로 거부됐다.원인은 무응답이 아니라 벽시계 예산 초과다. 검증 계획이 전체 스위트를 브랜치·base 두 번 돌리도록 했고, 이 호스트에서 한 번이 약 28분이라 56분 + 구현 시간이 1시간을 넘겼다.
원인
--timeout을 전달하지 않는다.lib_py/loop/runner.py의delegate_job이 넘기는 플래그는--agent-session·--agent·--type·--role·--registry-dir·--prompt뿐이다. heartbeat 계열만 env 로 전달한다(runner.py:385-390:MAM_HEARTBEAT_SEC·MAM_HEARTBEAT_MISSES·MAM_ACK_TIMEOUT_S).multi-agent-mux-delegate-job:135TIMEOUT=3600. 환경변수 fallback 이 없어--timeout플래그로만 바꿀 수 있는데, 루프 사용자에게는 그 경로가 없다.영향
deadline_timeout이라, 로그만 보면 "무응답"(K×N 초 이벤트 없음)과 구분되지 않는다. 이번 사례는 heartbeat 가 마지막까지 도착했는데도 같은 라벨이 붙었다.completed가 거부되어 job 기록이 결과와 어긋난다.제안
TIMEOUT에 env fallback 을 준다(예:MAM_JOB_TIMEOUT_S). heartbeat 계열과 같은 방식이라 일관된다.--job-timeout <sec>)으로도 노출한다.wall_clock_timeoutvsdeadline_timeout).관련
loop-20260921T023718Z-027d67)환경 정보
6fc9194(v4.7.0), Linux, Claude Code v2.1.270작업 시 감안할 점 — #17 항목 4 의 경계 기준과 처리 설계 (병합 워크스페이스, 2026-09-25)
경계 기준(#17 댓글 참조): 결정을 내리는 코드는 Python, 외부 프로세스를 만지는 코드는 bash. 벽시계·무응답 판정과 종결 라벨은 "결정" 이므로 Python 에 둔다. 지금은 두 시계를 Python 구독자(
job_subscriber.py)가 세고 라벨은 bash 래퍼(finalize_unfinished)가 붙이는 갈라진 구조라, 항목 4 가 종결 처리를 Python 으로 옮긴 뒤 이 이슈를 그 위에 얹는 순서를 권한다. 항목 4 보다 먼저 해야 한다면 bash 를 최소로만 고치고 라벨 결정은 Python 쪽 반환값(구독자 rc 2 에 어느 시계였는지 부호)으로 넘긴다.처리 설계 제안
progress --detail budget_exhausted발행 + 결정 파일(예:<jobs>/<jid>.extend) 대기(짧은 유예).MAM_JOB_EXTEND_MAX(기본 1)이면 같은 예산만큼 1회 연장). 루프 밖 직접 위임이면 오케스트레이터가 결정 파일을 쓴다.error / wall_clock_timeout으로 닫는다. 무응답(K×N 초 이벤트 없음)은 종전대로deadline_timeout. 두 라벨을 분리해야 사후 추론(events.log의 heartbeat 시각 비교) 없이 잡 레코드 한 줄로 구분된다.state_change_seq정체 등)을 연장 거부 근거로 쓴다.TIMEOUT에MAM_JOB_TIMEOUT_Sfallback, 루프--job-timeout <sec>전달. 기본 3600 은 유지(대부분의 잡에서 1시간 초과는 이상 신호). 연장 상한은 남긴다 — 무한 대기 방지 장치라는 뜻을 잃지 않도록.참고: 예산은 잡 하나 단위다(루프 사이클 전체가 아님). 관측 사례는 creator 잡 하나가 전체 스위트를 두 번 돌려 혼자 1시간을 넘긴 것이다.
PR #41 병합(v4.8.0)으로 닫힙니다. 예산(
--timeout>MAM_JOB_TIMEOUT_S> 3600, 루프--job-timeout)과 라벨(구독자 rc 4 →wall_clock_timeout, rc 2 →deadline_timeout)은 합의대로이며, 예산 연장은 #39 로 분리했습니다.설계 합의 (B): 네 리뷰어 모두 #39 분리에 동의(claude 조건부). 보완책(공통): 연장 횟수·누적 예산 상한은 오케스트레이터(소비자) 쪽에 두고, token/attempt 로 인증된 요청만 멱등 처리해 새 절대 deadline 을 확정·배포한다. heartbeat 만 있고
state_change_seq가 그대로면 거절한다(grok/agy). 원격 잡에도 같은 규칙. #39 에서 설계할 때 이 조건을 출발점으로 삼습니다.(D) 항목 4 로 옮길 bash 판정 목록(리뷰어 합의):
FINALIZE_CLOCK선택·디스크 유예/재관측 종료 결정, 감시 여부 판정(remote/human/MAM_STALL_WATCH),MAM_JOB_TIMEOUT_S검증·기본값(선행 0 이 bash 에서 8진수로 읽히는 문제 포함), rc→close_req/라벨 매핑,_map_sub_rc·cmd_wait정규화,start/stop_stall_watch수명 관리. bash 에는 spawn/wait/kill·키 주입·얇은 CLI 호환만 남깁니다. 문서 후속:MAM_WAIT_GRACE_S미문서, 러너의 잘못된MAM_JOB_TIMEOUT_S무언.