diff --git a/docs/contracts/issue-5/phase2b-autocomplete.md b/docs/contracts/issue-5/phase2b-autocomplete.md index d4d80f0..4d16ddd 100644 --- a/docs/contracts/issue-5/phase2b-autocomplete.md +++ b/docs/contracts/issue-5/phase2b-autocomplete.md @@ -63,6 +63,7 @@ Phase 2b가 제공하는 것은 **검증된 라이브러리와 게이트**이며 - **독립 Outbox 저장소를 만들지 않는다**(`state-transitions.md` §5 명시). 이미 쓰는 레코드에 필드 하나를 더 쓴다. - **발행 실패는 의도적으로 intent를 남긴다.** 보내지 않은 알림이 보낸 것처럼 보이는 편이, 재시도되는 편보다 나쁘다. - `notification_settled`는 **상태를 바꾸지 않는 bookkeeping 요청**이라 terminal 상태에서도 허용되며, **`pending_notification` 외의 필드는 쓸 수 없다**(경계에서 거부 — 현재 호출자가 그것만 넘긴다는 사실에 의존하지 않는다). +- **소비자 중복 제거**: 재발행은 **새 wire seq**를 받으므로 구독자의 단조 seq 가드는 재시도를 알아보지 못한다(가드 입장에서는 새 이벤트다). 그래서 `job_subscriber`가 `data.notification_intent`의 안정된 신원으로 중복을 제거한다. 재시작 후에는 디스크 폴백이 레지스트리에서 terminal을 확인하고, 중복 terminal은 전이 경계가 멱등 ACK로 거부하므로 이중 적용이 발생하지 않는다(교차 리뷰 Round 2 R4). - 정리는 **실제로 보낸 intent와 일치할 때만** 이루어진다. 발행은 락 밖에서 일어나므로 그 사이에 새 intent가 정당하게 들어올 수 있고, 무조건 지우면 아무도 보내지 않은 알림이 사라진다. - 발행 payload에 intent의 `seq`/`event`/`timestamp`를 실어 보낸다. **전송 계층 seq는 같은 intent의 재시도를 식별하지 못하므로**, 소비자는 `notification_intent`로 중복을 제거한다. 알림은 terminal에 *관한* 것이므로, terminal이 거부하는 요청으로는 영원히 정리할 수 없다. @@ -90,7 +91,9 @@ Phase 2b가 제공하는 것은 **검증된 라이브러리와 게이트**이며 2. `MAM_ENGINE_CAS=0` — Phase 2a 경로까지 되돌린다(자동완료는 그 위에 있으므로 함께 무력화). 3. `.mam/shadow/`는 레지스트리와 분리되어 있어 삭제해도 job 상태에 영향이 없다. -## 9. 계획 대비 이탈 1건 (레코드 스키마 미변경) +## 9. 계획 대비 이탈 (레코드 스키마) — **§8-1에서 부분 철회됨** + +> ⚠️ 아래 판단은 Round 2 교차 리뷰(`a45e5b7f` R2)로 **부분적으로 반증됐다.** 증거 계층의 대조만으로는 **커밋과의 원자성**을 얻을 수 없어, `dispatch_token`을 레코드에 추가했다(§8-1). 아래 서술은 "왜 `attempt_id`를 `expected_attempt`로 쓰지 않는가"에 한해 여전히 유효하다. 계획서 §D2는 `attempt_id`를 job 레코드의 1급 필드로 승격할 것을 권고하고, Phase 1이 동결한 "레코드 필드 불변" 계약의 변경이므로 **리뷰어 승인을 선행 조건**으로 걸었다. @@ -102,6 +105,23 @@ Phase 2b가 제공하는 것은 **검증된 라이브러리와 게이트**이며 `attempt_id` 승격은 열린 질문으로 남긴다. 승격하면 엔진에 2차 방어선이 생기지만 동결 계약을 바꿔야 하며, 현재는 필요하지 않다. `test_x03_the_record_carries_iteration_not_an_attempt_id`가 이 전제를 감시한다 — 레코드가 `attempt_id`를 갖게 되는 순간 실패하며, 그때가 재검토 시점이다. +## 8-1. 원자적 귀속 — dispatch token (교차 리뷰 Round 2 R2) + +**문제**: 귀속을 커밋 **전에** 확인하고 커밋에는 `expected_status="running"`만 걸면, 그 사이에 job이 `completed → redispatch → claim`으로 새 시도를 시작할 수 있고 **옛 증거가 새 시도를 완료시킨다.** 새 시도도 `running`이므로 상태 검사만으로는 구별되지 않는다. 실측으로 재현됐다(레지스트리가 이미 iteration 2인 상태에서 커밋됨). + +**해결**: 시도 정체성을 **레코드 안에** 둔다. 전제조건이 커밋과 원자적이려면 트랜잭션이 **자기 락 안에서 읽을 수 있어야** 하기 때문이다. + +| 항목 | 값 | +|---|---| +| 필드 | `dispatch_token` (불투명 문자열) | +| 발급 시점 | 시도가 시작되는 요청 — `claim`, `redispatch` | +| 사용 | 게이트가 판단에 사용한 토큰을 `expected_dispatch_token`으로 넘기고, 엔진이 **락 안에서** 대조 | +| 증거 결속 | shadow dispatch 레코드도 같은 토큰을 보관 → 증거가 어느 시도의 것인지 증명된다 | + +**왜 `iteration`이 아니라 토큰인가**: `iteration`은 이미 **루프 회차**를 뜻한다. dispatch 시도 번호를 그것과 비교하면 값이 우연히 같을 때는 공허하고, 갈라지면 **정상 완료를 거부**한다(3회차 loop job에서 실측). 불투명 토큰은 경쟁하는 의미가 없다. + +**이전 판단의 정정**: 아래 §9는 "시도 보증이 증거 계층에 있으므로 레코드 스키마 변경이 불필요하다"고 적었다. 증거와 dispatch를 **대조**하는 데는 맞지만, **커밋과의 원자성**에는 틀렸다. 원자성은 트랜잭션이 읽을 수 있는 곳에 값이 있어야만 얻어지며, 그것이 레코드다. `dispatch_token`은 추가 필드이므로 `SCHEMA_VERSION`은 1을 유지한다. + ## 9-1. 구현 중 정정 1건 — `finalize`의 패키지 위치 처음에는 `finalize.py`를 `lib_py/loop/`에 두었는데, 그러면 `publish_event`(=`lib_py.delegation`)가 **형제 패키지를 임포트**하게 되어 freeze 하에서 트리가 섞인다. Phase 1이 명문화하고 Phase 2a에서 엔진 배치를 정정하게 만들었던 바로 그 규칙이며, 기존 가드 테스트(`test_delegation_package_imports_no_sibling_lib_py_modules`)가 전체 스위트에서 이를 검출했다. diff --git a/docs/contracts/issue-5/state-transitions.md b/docs/contracts/issue-5/state-transitions.md index 87e5256..bb0acec 100644 --- a/docs/contracts/issue-5/state-transitions.md +++ b/docs/contracts/issue-5/state-transitions.md @@ -1,6 +1,6 @@ # 📑 Target State Transitions and CAS Specification -- **Document Version**: Rev.5 (멱등 ACK 구분 및 관측 증거 필수 요건 명문화) +- **Document Version**: Rev.6 (dispatch token 기반 원자적 시도 귀속 추가) - **Issue**: [Gitea Issue #5](https://git.godopu.com/tmpl/multi-agent-mux/issues/5) - **Scope**: Target Compare-And-Swap (CAS) State Machine & Attribution Contract @@ -54,6 +54,8 @@ $$\text{Attribution Tuple} = (\text{job\_id}, \text{attempt\_id}, \text{agent\_s > 🔑 **권한 축(Authority)**: `redispatch`·`finalize`는 `source ∈ {registry_cli, operator}`에서만 허용된다. 에이전트(`publish_event`)는 **자신의 terminal을 되돌릴 수 없다** — 이것이 CAS 표의 존재 이유이며, 위 두 행은 그 원칙의 예외가 아니라 래퍼가 수행하는 별개 전이를 이름 붙인 것이다. > +> 🔑 **시도 정체성은 레코드에 있다**: `dispatch_token`은 시도를 시작하는 요청(`claim`/`redispatch`)에서 새로 발급되며, `expected_dispatch_token` 전제조건으로 **락 안에서** 대조된다. 커밋 전에만 확인하는 귀속 검사는 그 사이의 재디스패치를 막지 못한다 — 전제조건이 커밋과 원자적이려면 트랜잭션이 자기 락 안에서 읽을 수 있어야 한다. `iteration`(루프 회차)과는 다른 개념이므로 별도 필드다. — 교차 리뷰 `a45e5b7f` R2 +> > 🔑 **멱등 ACK는 거부가 아니다**: `terminal` 상태에서 같은 terminal을 재수신하면 표는 이를 **No-op ACK**로 정의한다. 호출자(운영자 CLI)는 이를 성공(rc 0)으로 취급해야 한다. 실제 거부(상충 신호·권한 부족)만 non-zero다. — 교차 리뷰 `cb792e91` F-C > > 🔑 **status는 요청으로만 설정된다**: 임의 필드로 실린 `status`는 경계 내부에서 거부된다(`progress`가 terminal을 커밋하는 우회 차단). 알 수 없는 요청명도 거부된다. `pending`도 소유 요청(`redispatch`)을 갖는다. diff --git a/docs/contracts/issue-5/task.md b/docs/contracts/issue-5/task.md index c4cfef9..56c67d2 100644 --- a/docs/contracts/issue-5/task.md +++ b/docs/contracts/issue-5/task.md @@ -107,6 +107,9 @@ - [x] **P2B-05** 교차 리뷰 3인(`3fa76e08` Claude / `a0de0e5e` Grok / `e5f26823` Codex) 차단 지적 7건 전건 수용·수정. - *증거*: 커밋 `cb7ed1a`. 필수 매니페스트 검증(`report_dir` 생략 = 거부), 증거의 대상 일치(job/attempt/session/pane), Reviewer 판정 토큰 게이트(Phase 0 안전 계약), bookkeeping 필드 제한, 조건부 settle + intent 신원 전달, 부수효과 evidence 스키마 검증, `MAM_ENGINE_CAS=0` 준수, 백엔드 정확 일치, 절대 경로. 신규 회귀 21종 전부 수정 전 코드에서 실패함을 역검증. - *배선 현황*: `evaluate`/`recover`의 **프로덕션 호출자는 아직 없다**(Phase 3 배선). 운영 노트 §2-1에 정정 기록. +- [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-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.