fix(issue-5): record an accepted intent, not a refused one

The subscriber marked a notification intent as seen before the monotonic
sequence guard ran. A late packet was then dropped by that guard with its
intent already burned, so the genuine retry of a notification this
subscriber had never delivered was mistaken for a replay and dropped too.

Identity comparison stays ahead of the sequence guard -- a duplicate must
not consume a sequence number -- but the bookkeeping write moves behind
it, so "seen" means "accepted" rather than "arrived".

An intent that cannot yield a stable identity (not an object, or holding
an unhashable value) is now refused instead of raising out of the
callback: a paho callback that raises takes the network loop down for
every job that subscriber is watching.

Six regressions; three of them fail on the unfixed tree, and one drives
a real notifier retry through the real consumer with no hand-built
payload. Full suite: 732 passed, no skips.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
GodopuandClaude Opus 5 committed 2026-09-07 08:11:37 +09:00
1 parent 9a651ccc71
commit 8d7e010f9f
4 files changed
+162 -5

No files matched your search

+4 -1
View File
@@ -110,9 +110,12 @@
- [x] **P2B-06** 교차 리뷰 Round 2(`a45e5b7f` Codex) 지적 2건 수용·수정.
- *증거*: 커밋 `01b465f`. R2(귀속이 커밋과 원자적이지 않음 → `dispatch_token`을 레코드에 도입, `claim`/`redispatch`에서 발급, `expected_dispatch_token`으로 락 안에서 대조), R4(intent 신원을 발행만 하고 소비자가 무시 → `job_subscriber`가 안정 신원으로 중복 제거). 신규 회귀 7종 전부 수정 전 코드에서 실패함을 역검증.
- *이전 판단 정정*: "시도 보증이 증거 계층에 있으므로 레코드 스키마 변경 불필요"는 **대조**에는 맞지만 **원자성**에는 틀렸다. 추가 필드이므로 `SCHEMA_VERSION`은 1 유지.
- [x] **P2B-07** 교차 리뷰 Round 3(Codex) 지적 1건 + 그 과정에서 드러난 인접 결함 1건 수용·수정.
- *증거*: `job_subscriber._Watcher.on_message`의 `seen_intents` 기록이 단조 seq 가드보다 **앞서** 실행되어, 늦게 도착한 과거 패킷이 자기 intent를 "이미 처리함"으로 태우고 **뒤따르는 정당한 재시도가 드롭**됐다(`last_seq=10`, seq=9 거부 후 동일 intent의 seq=11 → `qsize()==0`). 신원 **대조**는 seq 가드 앞에, 신원 **기록**은 seq 가드 뒤에 두어 "본 적 있다"가 "받아들였다"를 뜻하도록 고쳤다. 함께: 해시 불가능·비객체 intent가 `TypeError`로 콜백을 빠져나가 **그 구독자의 모든 job 수신을 끊을 수 있던** 경로를 드롭+경고로 닫았다. 신규 회귀 6종(그중 3종은 수정 전 코드에서 실패함을 역검증, 3종은 보존·커버리지 가드) + 발행→소비 실경로 종단 테스트.
- [x] **P2B-04** 부수효과 예외·무판정 fail-closed·경쟁/재시작 시나리오.
- *증거*: 신규 시나리오 3종(`side_effect_unproven`, `no_verdict_fail_closed`, `notification_crash_window`)을 Phase 0 스키마로 추가, **총 15종 전부** `expected_policy.decision` 일치.
- *계획 대비 이탈*: 계획서 §D2의 `attempt_id` 레코드 승격(및 그 선행 조건인 리뷰어 승인)은 **불필요함이 실측으로 확인되어 수행하지 않았다.** 시도 보증은 이미 증거 계층(`policy` ↔ shadow dispatch 대조)에 있으며, 레코드 스키마와 Phase 1 동결 계약은 그대로다. 상세는 `phase2b-autocomplete.md` §9.
- *계획 대비 이탈(이후 정정됨)*: 처음에는 계획서 §D2의 `attempt_id` 레코드 승격이 불필요하다고 판단했으나, **P2B-06에서 이 판단의 절반을 철회했다.** 증거를 dispatch와 *대조*하는 데에는 증거 계층으로 충분하지만 커밋과의 *원자성*에는 부족하다. 최종적으로 승격한 것은 `attempt_id`가 아니라 **불투명한 `dispatch_token`**이다(`iteration`은 루프 회차라는 다른 뜻을 이미 갖고 있다). 레코드 스키마는 필드 추가만이며 `SCHEMA_VERSION`은 1 유지. 상세는 `phase2b-autocomplete.md` §9(부분 철회 표시).
---