docs(issue-5): record atomic attribution and correct my earlier claim
The state-transition spec gains the rule that attempt identity lives in the record, because a precondition can only be atomic with the commit if the transaction can read it under its own lock, and checking attribution beforehand leaves a window a re-dispatch fits through. The operations note gets the same, plus the reason it is an opaque token rather than the iteration counter: iteration already means the loop round, so comparing a dispatch attempt against it is vacuous when they agree and wrong when they diverge. It also marks my earlier section as partly withdrawn rather than quietly editing it. That section argued a record schema change was unnecessary because the attempt guard lives in the evidence layer. That was right about comparing evidence to a dispatch and wrong about atomicity, and the review proved the difference by execution. The narrower point it made -- why not to reuse iteration -- still holds and is kept. Also records why the subscriber now deduplicates on the intent identity: a replay arrives with a fresh wire sequence, so the monotonic guard cannot see it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
1 parent
01b465f50c
commit
9a651ccc71
3 files changed
+27
-2
No files matched your search
@@ -63,6 +63,7 @@ Phase 2b가 제공하는 것은 **검증된 라이브러리와 게이트**이며
|
|||||||
- **독립 Outbox 저장소를 만들지 않는다**(`state-transitions.md` §5 명시). 이미 쓰는 레코드에 필드 하나를 더 쓴다.
|
- **독립 Outbox 저장소를 만들지 않는다**(`state-transitions.md` §5 명시). 이미 쓰는 레코드에 필드 하나를 더 쓴다.
|
||||||
- **발행 실패는 의도적으로 intent를 남긴다.** 보내지 않은 알림이 보낸 것처럼 보이는 편이, 재시도되는 편보다 나쁘다.
|
- **발행 실패는 의도적으로 intent를 남긴다.** 보내지 않은 알림이 보낸 것처럼 보이는 편이, 재시도되는 편보다 나쁘다.
|
||||||
- `notification_settled`는 **상태를 바꾸지 않는 bookkeeping 요청**이라 terminal 상태에서도 허용되며, **`pending_notification` 외의 필드는 쓸 수 없다**(경계에서 거부 — 현재 호출자가 그것만 넘긴다는 사실에 의존하지 않는다).
|
- `notification_settled`는 **상태를 바꾸지 않는 bookkeeping 요청**이라 terminal 상태에서도 허용되며, **`pending_notification` 외의 필드는 쓸 수 없다**(경계에서 거부 — 현재 호출자가 그것만 넘긴다는 사실에 의존하지 않는다).
|
||||||
|
- **소비자 중복 제거**: 재발행은 **새 wire seq**를 받으므로 구독자의 단조 seq 가드는 재시도를 알아보지 못한다(가드 입장에서는 새 이벤트다). 그래서 `job_subscriber`가 `data.notification_intent`의 안정된 신원으로 중복을 제거한다. 재시작 후에는 디스크 폴백이 레지스트리에서 terminal을 확인하고, 중복 terminal은 전이 경계가 멱등 ACK로 거부하므로 이중 적용이 발생하지 않는다(교차 리뷰 Round 2 R4).
|
||||||
- 정리는 **실제로 보낸 intent와 일치할 때만** 이루어진다. 발행은 락 밖에서 일어나므로 그 사이에 새 intent가 정당하게 들어올 수 있고, 무조건 지우면 아무도 보내지 않은 알림이 사라진다.
|
- 정리는 **실제로 보낸 intent와 일치할 때만** 이루어진다. 발행은 락 밖에서 일어나므로 그 사이에 새 intent가 정당하게 들어올 수 있고, 무조건 지우면 아무도 보내지 않은 알림이 사라진다.
|
||||||
- 발행 payload에 intent의 `seq`/`event`/`timestamp`를 실어 보낸다. **전송 계층 seq는 같은 intent의 재시도를 식별하지 못하므로**, 소비자는 `notification_intent`로 중복을 제거한다. 알림은 terminal에 *관한* 것이므로, terminal이 거부하는 요청으로는 영원히 정리할 수 없다.
|
- 발행 payload에 intent의 `seq`/`event`/`timestamp`를 실어 보낸다. **전송 계층 seq는 같은 intent의 재시도를 식별하지 못하므로**, 소비자는 `notification_intent`로 중복을 제거한다. 알림은 terminal에 *관한* 것이므로, terminal이 거부하는 요청으로는 영원히 정리할 수 없다.
|
||||||
|
|
||||||
@@ -90,7 +91,9 @@ Phase 2b가 제공하는 것은 **검증된 라이브러리와 게이트**이며
|
|||||||
2. `MAM_ENGINE_CAS=0` — Phase 2a 경로까지 되돌린다(자동완료는 그 위에 있으므로 함께 무력화).
|
2. `MAM_ENGINE_CAS=0` — Phase 2a 경로까지 되돌린다(자동완료는 그 위에 있으므로 함께 무력화).
|
||||||
3. `.mam/shadow/`는 레지스트리와 분리되어 있어 삭제해도 job 상태에 영향이 없다.
|
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이 동결한 "레코드 필드 불변" 계약의 변경이므로 **리뷰어 승인을 선행 조건**으로 걸었다.
|
계획서 §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`를 갖게 되는 순간 실패하며, 그때가 재검토 시점이다.
|
`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`의 패키지 위치
|
## 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`)가 전체 스위트에서 이를 검출했다.
|
처음에는 `finalize.py`를 `lib_py/loop/`에 두었는데, 그러면 `publish_event`(=`lib_py.delegation`)가 **형제 패키지를 임포트**하게 되어 freeze 하에서 트리가 섞인다. Phase 1이 명문화하고 Phase 2a에서 엔진 배치를 정정하게 만들었던 바로 그 규칙이며, 기존 가드 테스트(`test_delegation_package_imports_no_sibling_lib_py_modules`)가 전체 스위트에서 이를 검출했다.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 📑 Target State Transitions and CAS Specification
|
# 📑 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)
|
- **Issue**: [Gitea Issue #5](https://git.godopu.com/tmpl/multi-agent-mux/issues/5)
|
||||||
- **Scope**: Target Compare-And-Swap (CAS) State Machine & Attribution Contract
|
- **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 표의 존재 이유이며, 위 두 행은 그 원칙의 예외가 아니라 래퍼가 수행하는 별개 전이를 이름 붙인 것이다.
|
> 🔑 **권한 축(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
|
> 🔑 **멱등 ACK는 거부가 아니다**: `terminal` 상태에서 같은 terminal을 재수신하면 표는 이를 **No-op ACK**로 정의한다. 호출자(운영자 CLI)는 이를 성공(rc 0)으로 취급해야 한다. 실제 거부(상충 신호·권한 부족)만 non-zero다. — 교차 리뷰 `cb792e91` F-C
|
||||||
>
|
>
|
||||||
> 🔑 **status는 요청으로만 설정된다**: 임의 필드로 실린 `status`는 경계 내부에서 거부된다(`progress`가 terminal을 커밋하는 우회 차단). 알 수 없는 요청명도 거부된다. `pending`도 소유 요청(`redispatch`)을 갖는다.
|
> 🔑 **status는 요청으로만 설정된다**: 임의 필드로 실린 `status`는 경계 내부에서 거부된다(`progress`가 terminal을 커밋하는 우회 차단). 알 수 없는 요청명도 거부된다. `pending`도 소유 요청(`redispatch`)을 갖는다.
|
||||||
|
|||||||
@@ -107,6 +107,9 @@
|
|||||||
- [x] **P2B-05** 교차 리뷰 3인(`3fa76e08` Claude / `a0de0e5e` Grok / `e5f26823` Codex) 차단 지적 7건 전건 수용·수정.
|
- [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종 전부 수정 전 코드에서 실패함을 역검증.
|
- *증거*: 커밋 `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에 정정 기록.
|
- *배선 현황*: `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·경쟁/재시작 시나리오.
|
- [x] **P2B-04** 부수효과 예외·무판정 fail-closed·경쟁/재시작 시나리오.
|
||||||
- *증거*: 신규 시나리오 3종(`side_effect_unproven`, `no_verdict_fail_closed`, `notification_crash_window`)을 Phase 0 스키마로 추가, **총 15종 전부** `expected_policy.decision` 일치.
|
- *증거*: 신규 시나리오 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` 레코드 승격(및 그 선행 조건인 리뷰어 승인)은 **불필요함이 실측으로 확인되어 수행하지 않았다.** 시도 보증은 이미 증거 계층(`policy` ↔ shadow dispatch 대조)에 있으며, 레코드 스키마와 Phase 1 동결 계약은 그대로다. 상세는 `phase2b-autocomplete.md` §9.
|
||||||
|
|||||||
Reference in new issue
Block a user