diff --git a/docs/contracts/issue-5/phase2a-shadow.md b/docs/contracts/issue-5/phase2a-shadow.md index f69e37a..88a354a 100644 --- a/docs/contracts/issue-5/phase2a-shadow.md +++ b/docs/contracts/issue-5/phase2a-shadow.md @@ -24,6 +24,15 @@ Shadow 러너는 애초에 쓰기 경로가 없으므로 **프로세스 종료만으로 흔적이 사라진다.** `.mam/shadow/`는 레지스트리와 분리되어 있어 삭제해도 job 상태에 영향이 없다. +## 2-1. 권한 구분 (교차 리뷰 반영) + +| source | 할 수 있는 것 | 할 수 없는 것 | +|---|---|---| +| `publish_event` (에이전트) | 자신의 작업을 `running`/`completed`/`error`/`cancelled`로 전이 | **terminal을 되돌리는 것** (`redispatch`/`finalize` 거부) | +| `registry_cli` (래퍼·운영자) | 위 전부 + `redispatch`(terminal→`pending`, iteration 증가 필수) + `finalize`(terminal→`error`) | iteration을 되돌리는 재생 | + +**CLI 종료 코드 변경**: `registry.py status`/`update`가 CAS에 의해 거부되면 이제 **stderr에 사유를 내고 non-zero(4)** 로 종료한다. Phase 2a 이전에는 거부가 불가능해 항상 0이었고, 그 침묵이 반복 위임이 조용히 깨진 것을 감췄다. + ## 3. 락 규율 (P2A-01) ``` @@ -31,7 +40,7 @@ with registry_lock: 판정 + 레코드 1회 쓰기 + 알림 큐잉 ← 락 해제 후: 감사 로그 flush, 발행 등 느린 작업 ``` -감사 mirror가 락 밖으로 나가면서 순서 보장을 잃을 수 있으므로, 전이마다 단조 증가하는 `transition_seq`를 레코드와 알림에 함께 실어 늦게 도착한 flush도 순서를 복원할 수 있게 했다. 이 형태는 새로 만든 것이 아니라 `pick_pending`이 이미 쓰던 패턴을 모든 전이로 일반화한 것이다. +감사 mirror가 락 밖으로 나가면서 순서 보장을 잃을 수 있으므로, 전이마다 단조 증가하는 `transition_seq`를 레코드와 알림에 함께 싣는다. **`update_logged_status`는 기존 `status.json`의 `transition_seq`가 더 크거나 같으면 쓰기를 건너뛴다** — 지연된 flush가 최신 상태를 되돌리지 못하게 하는 것이며, 락이 공짜로 주던 순차 일관성을 복원한다(교차 리뷰 F-B). 이 형태는 새로 만든 것이 아니라 `pick_pending`이 이미 쓰던 패턴을 모든 전이로 일반화한 것이다. ## 4. `herdr agent wait` 사용 규칙 @@ -61,3 +70,5 @@ Phase 0의 7종 + Phase 2a의 5종 = **12종**이 `expected_policy.decision`으 2. `policy.decide`의 `candidate_approved`를 `engine.transition("completed", expected_attempt=...)`에 연결. 엔진과 CAS 표는 이미 준비되어 있다. 3. Pending Notification 지연 알림·크래시 복구 (`state-transitions.md` §5). 4. `preexec_fn`: Phase 2a는 데몬을 스폰하지 않아 미노출. 2b가 스폰을 도입하면 스레드 안전한 방식으로 교체해야 한다. +5. **`started`의 세션 확인 (O-A)**: `_decide`의 `claim`/`started`는 아직 "작업 할당 세션 일치"를 검사하지 않는다(`pick_pending`은 세션 필터를 하지만 `publish_event`의 `started`는 누구나 보낼 수 있다). 2b에서 `expected_session`을 추가할 것. +6. **`stop_session.sh`의 무조건 `cancelled` (O-B)**: 이미 `completed`인 job에 대해 이제 거부된다(레거시는 덮어썼다). 표상 의도된 동작이며, 운영자가 확정 상태를 바꾸려면 `registry.py status --set`(운영자 권한)을 쓴다. 릴리스 노트 기록 대상. diff --git a/docs/contracts/issue-5/state-transitions.md b/docs/contracts/issue-5/state-transitions.md index 736fcac..5f9cd08 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.3 (Phase 2a 구현 매핑 추가) +- **Document Version**: Rev.4 (운영자 권한 전이 2행 및 권한 축 추가 — 교차 리뷰 반영) - **Issue**: [Gitea Issue #5](https://git.godopu.com/tmpl/multi-agent-mux/issues/5) - **Scope**: Target Compare-And-Swap (CAS) State Machine & Attribution Contract @@ -49,6 +49,12 @@ $$\text{Attribution Tuple} = (\text{job\_id}, \text{attempt\_id}, \text{agent\_s | `terminal` | 상충 신호 (`progress`/다른 Terminal) | 이미 Terminal 상태에 도달한 후 늦게 도착한 신호 | 기 커밋 상태 유지 | 상태 역행 및 덮어쓰기 거부, Conflict Audit Log 기록 | | 임의 상태 | Stale Attempt / Generation | 과거 시도 세대 또는 프로세스 세대 불일치 신호 | 전이 거부 | Stale Attempt 경고 기록 및 패킷 폐기 | | `running` | `deadline_timeout` | 작업 타임아웃 경과 및 실행 중단 확인 | `error` | 타임아웃 원인 기록 | +| `terminal` | `redispatch` (**운영자 권한**) | 반복 위임(`--type loop\|discuss`)의 다음 회차 전달. `iteration`이 기존 값보다 **엄격히 증가**해야 함 | `pending` | 교차 리뷰 `68abd0ff` 보완. 재생(replay)은 iteration 미증가로 거부됨 | +| `terminal` | `finalize` (**운영자 권한**) | 반복 예산 소진 시 워크플로 확정 | `error` | 교차 리뷰 `68abd0ff` 보완 | + +> 🔑 **권한 축(Authority)**: `redispatch`·`finalize`는 `source ∈ {registry_cli, operator}`에서만 허용된다. 에이전트(`publish_event`)는 **자신의 terminal을 되돌릴 수 없다** — 이것이 CAS 표의 존재 이유이며, 위 두 행은 그 원칙의 예외가 아니라 래퍼가 수행하는 별개 전이를 이름 붙인 것이다. +> +> 🔑 **status는 요청으로만 설정된다**: 임의 필드로 실린 `status`는 경계 내부에서 거부된다(`progress`가 terminal을 커밋하는 우회 차단). 알 수 없는 요청명도 거부된다. `pending`도 소유 요청(`redispatch`)을 갖는다. --- @@ -56,10 +62,10 @@ $$\text{Attribution Tuple} = (\text{job\_id}, \text{attempt\_id}, \text{agent\_s | 계약 | 구현 | 테스트 | |---|---|---| -| §3 CAS 전이표 | `lib_py/delegation/engine.py::_decide` (순수 함수, 락 안에서 호출) | `test_engine_*` | +| §3 CAS 전이표 | `lib_py/delegation/engine.py::_decide` (순수 함수, 락 안에서 호출) | `test_engine_*`, `test_fa_*`, `test_r2_*`, `test_r3_*` | | 단일 전이 주체 | `engine.transition` — 유일한 상태 writer. `update_job_status`·`pick_pending`이 이를 경유 | `test_g6_every_record_writer_*` | | 락 내 I/O 배제 | 락 = 판정 + 레코드 1회 쓰기. 감사 mirror는 큐잉 후 락 밖 flush | `test_g5_no_blocking_io_*` | -| §2 4중 귀속 | `lib_py/loop/attribution.py::check` | `test_attribution_*` | +| §2 4중 귀속 | `lib_py/loop/attribution.py::check` + `policy.collect_observed_identity` (관측 식별자와의 대조) | `test_attribution_*`, `test_r1_*` | | 롤백 | `MAM_ENGINE_CAS=0` → 보존된 `_*_legacy` 본체 | 전 특성화 테스트 parametrize | > 📌 **Phase 2a는 추론 완료를 커밋하지 않는다.** `inferred completed` 행은 `lib_py/loop/policy.py`가 `candidate_approved`로 **판정만** 하고, shadow 러너가 이를 로그로 남긴다. 실제 CAS 커밋 연동은 Phase 2b다. diff --git a/docs/contracts/issue-5/task.md b/docs/contracts/issue-5/task.md index 1bce54d..d69e80f 100644 --- a/docs/contracts/issue-5/task.md +++ b/docs/contracts/issue-5/task.md @@ -85,6 +85,8 @@ - *증거*: 커밋 `b49af56`. `--timeout`은 **밀리초**(파라미터명 `timeout_ms`), `--until` 항상 명시(생략 시 `working` 누락), exit 1은 **stderr**에서 파싱. 백오프 1→2→4→8→16s 상한, 실패 시 캐시 스냅샷 폐기. 귀속 증거는 **레지스트리가 아닌 shadow 전용 저장소**(`.mam/shadow/`)에 둔다 — 레코드에 3개 필드가 전부 없고 Phase 1이 스키마를 동결했기 때문. - [x] **P2A-03** shadow-only가 terminal을 쓰지 않음을 검증. - *증거*: 커밋 `964abc6`. `ShadowRunner`는 engine 참조를 **갖지 않는다**(구조적 차단, AST 단정). 측정: 전체 시나리오 실행 전후 **레지스트리 디렉터리 전 파일 SHA-256 동일**. +- [x] **P2A-05** 교차 리뷰(`68abd0ff` Claude / `cae12436` Codex) 지적 5건 전건 수용·수정. + - *증거*: 커밋 `56d08ef`. F-A(래퍼 loop/discuss 전이 2건 누락 → 운영자 권한 요청 `redispatch`/`finalize` 신설 + CLI 거부 시 non-zero), F-B(감사 mirror 순서 회귀 → `transition_seq` 비교), R1(관측 식별자 미대조 → `collect_observed_identity` 전량 대조 + 매니페스트 attempt 검증), R2(`expected_attempt`가 레코드에 없으면 무시 → fail-closed), R3(raw `status` 필드 우회 → 요청만이 status를 설정). 신규 회귀 11종 전부 수정 전 코드에서 실패함을 역검증. - [x] **P2A-04** race/attempt 재사용/부분 report/도구 실행/blocked/unknown/재시작 시나리오. - *증거*: 신규 5종(`race_double_dispatch`, `attempt_reuse`, `partial_report`, `unknown_status`, `restart_midflight`)을 Phase 0 스키마로 작성해 **Phase 0의 `validate_watcher_scenario`를 통과**시켰고, 기존 7종과 합쳐 **12종 전부 `expected_policy.decision`과 일치**함을 단정한다.