7.1 KiB
Issue #5 Phase 2b — Round 4 교차 리뷰 (Codex Round 3 R4 후속 결함 수정)
- Job: a665d729 / Reviewer: planner-reviewer-claude-01
- 리뷰 대상: 커밋
8d7e010"fix(issue-5): record an accepted intent, not a refused one" (기준9a651cc) - 범위 고정: brief에 내장된 diff와
git diff 9a651cc..HEAD -- . ':(exclude).agents/reports'를 바이트 비교 → 동일(15,916 B). 변경 파일 4개:lib_py/delegation/job_subscriber.py,docs/contracts/issue-5/phase2b-autocomplete.md,docs/contracts/issue-5/task.md,tests/test_issue5_phase2b_autocomplete.py. - 동결 스냅샷:
mam-loop-freeze.03Uqb9의job_subscriber.pymd5 =9a651cc시점과 동일(수정 전). engine/notifier/autocomplete/lib.sh는 HEAD와 동일. 즉 이 루프 자신의 구독자 데몬(pid 10863)은 수정 전 코드를 실행 중이며, 수정은 다음 루프 기동(새 동결)부터 효력을 갖는다. B-13 설계상 당연한 결과이고 diff의 결함이 아니다.
1. 지적 사항 대조 (Codex Round 3)
지적: _Watcher.on_message에서 seen_intents[jid].add(identity)가 단조 seq 가드보다 앞서 실행되어, 늦게 도착한 과거 패킷이 자기 intent를 "본 것"으로 태우고 뒤따르는 정당한 재시도가 드롭된다.
수정 후 순서(job_subscriber.py:144-179, 직접 확인):
data가 dict가 아니면 intent 없음으로 취급(이전 코드의(data or {}).get경로 제거)- intent가 있으나 dict가 아니면 드롭+경고
- identity 튜플 생성 →
in seen_intents대조를try/except TypeError로 감싸 해시 불가면 드롭+경고 - 이미 본 identity면 드롭(seq 바닥을 올리지 않음)
- seq 단조 가드
- 가드 통과 후에만
seen_intents[jid].add(identity) - 감사 로그
received기록 → 큐 투입
brief의 요구 2)·3) 모두 충족. 요구 3)의 시나리오(last_seq=10, seq=9 거부 → 동일 intent seq=11 수신 → qsize()==1)가 test_r4b_a_stale_packet_does_not_burn_the_intent_for_its_own_retry에 그대로 있다.
2. 실측 재현
2-1. 신규 테스트 6종 — HEAD vs 수정 전 트리
9a651cc 워크트리에 HEAD의 테스트 파일을 복사해 실행:
| 결과 | 테스트 |
|---|---|
| 수정 전 FAIL | stale_packet_does_not_burn_the_intent (seen_intents == {(7,'completed','T')} 잔존), unhashable_intent_is_refused (TypeError: unhashable type: 'list' 콜백 탈출), non_object_intent_is_refused |
| 수정 전 PASS(보존 가드) | duplicate_intent_does_not_advance_the_sequence_floor, absent_or_null_intent, real_retry_is_deduplicated_by_the_real_consumer |
task.md P2B-07의 "3종 역검증 실패 / 3종 보존" 주장과 정확히 일치. HEAD에서는 파일 전체 65 passed.
2-2. 테스트 밖 엣지 — 실제 콜백에 직접 투입 (HEAD / 수정 전)
| 입력 | 수정 전 | HEAD |
|---|---|---|
data가 비어있지 않은 list / str |
AttributeError 콜백 탈출 |
정상 수신(intent 없음 취급) |
intent의 event 필드가 dict(중첩 해시 불가) |
TypeError 콜백 탈출 |
드롭+경고, last_seq 불변 |
| 같은 wire seq QoS1 재전달(+intent) | 1건 | 1건 |
| 거부된 replay(seq 9) 뒤 바닥(1)보다 크고 9보다 작은 정상 이벤트(seq 4) | 수신 | 수신 |
필드가 전부 없는 intent {} 두 번 |
1건 수신, (None,None,None) 기록 |
동일 (O-1 참조) |
data가 비객체인 경우는 task.md가 주장하지 않은 추가 탈출 경로인데 이번 수정으로 함께 닫혔다(paho 네트워크 스레드에서 콜백 예외는 해당 구독자의 모든 job 수신을 끊으므로 실제 가치가 있다).
2-3. 종단 테스트의 전제 확인
test_r4b_a_real_retry_...는 도달 불가 브로커(127.0.0.1:65432)로 notifier.flush 2회 후 감사 로그의 published 항목 2건을 실제 콜백에 투입한다. publish_event.record_outcome이 발행 성공 여부와 무관하게 published 감사 항목을 기록(G-1/G-2/G-4)함을 코드로 확인했으므로 전제가 성립하며, 두 항목의 wire seq가 다르고 notification_intent가 같음 → 1건만 큐에 남는다.
2-4. 린트·정합성
py_compileOK. pyflakes/ruff는 venv에 없고 저장소에 line-length 설정도 없음(pyproject/setup.cfg/ruff.toml 부재). 신규 줄 중job_subscriber.py:161(102자)이 100자를 넘지만 기존 줄(98·170·227·228)도 이미 넘으므로 관례 위반 아님.- 테스트 실행 후
git statusclean(샌드박스에 격리됨). HEAD 수집 총계 732 = 726 + 6.
3. 판단
결함 없음(blocking). 지적된 순서 역전은 "대조는 가드 앞, 기록은 가드 뒤"로 정확히 고쳐졌고, 그 논리(중복은 시퀀스를 소비할 자격이 없고, 전달 안 된 이벤트는 본 것이 아니다)가 코드 주석·계약 문서·테스트에 일관되게 적혀 있다. 방어적 파싱 강화는 콜백 예외 탈출 경로 3가지(비객체 intent, 해시 불가 필드, 비객체 data)를 닫았다.
4. 비차단 관찰 (다음 라운드/Phase 3 참고)
- O-1. 식별 필드가 전부 없는 intent는 "안정된 신원"이 아니다.
{}나{"foo":1}은 identity(None,None,None)을 얻어 첫 번째만 수신되고 이후 같은 꼴은 모두 replay로 드롭된다. 정상 발행자(notifier.intent_for)는 세 필드를 항상 채우므로 실제 경로에서는 발생하지 않고, 발생하더라도 방향이 fail-closed(드롭)라 이중 적용은 없다. 다만 phase2b-autocomplete.md의 새 문장 "안정된 신원을 만들 수 없는 intent는 거부한다"와 코드가 정확히 맞으려면identity생성 직후if any(f is None for f in identity): 드롭+경고한 줄이면 된다. 검증: HEAD 워크트리에 이 4줄을 적용하고 phase2b·phase1 테스트 131 passed,{}/{"foo":1}는 드롭되고 정상 intent는 1회 수신됨을 확인. - O-2. task.md P2B-07 문구의 개수. "신규 회귀 6종(3종 실패 역검증, 3종 보존) + 종단 테스트"로 읽히지만 종단 테스트는 6종에 포함된다(
test_r4b_함수는 정확히 6개, 보존 3종 중 하나가 종단 테스트). "6종(그중 1종은 종단)"으로 고치면 된다. - O-3. 운영 노트. 동결 스냅샷 원리상 현재 실행 중인 루프의 구독자는 수정 전 코드다. 이 라운드의 알림 재시도가 stale-first 상황을 만나면 옛 결함이 재현될 수 있으나, 재시작 후 디스크 폴백 + 멱등 ACK가 종단을 보증한다(기존 계약). 조치 불필요, 인지만.
- 이전 라운드 인계 사항(O-7 direct 잡의
claim토큰 발급,_delegate_py_bin앵커링, redispatch 시 shadow dispatch 갱신,preexec_fn)은 이번 diff 범위 밖이며 그대로 유효하다.
5. 전체 스위트
.venv/bin/python -m pytest tests -q -p no:cacheprovider (백그라운드 1회, 정상 종료): 732 passed in 802.02s (0:13:22), FAILED/ERROR 0건. 이전 라운드 726 → 732 (+6, 신규 test_r4b_* 6종). 이번에는 이전 라운드(19ac4c53)에서 있었던 비정상 종료가 재현되지 않았다.
[VERDICT: PASS]