docs(issue-5): record the operator-authority transitions and the review fixes
state-transitions.md §3 gains the two rows it was missing. They are named transitions, not exceptions: re-dispatching a finished job to the next iteration and finalising an exhausted one are things the wrapper legitimately does, and they are gated on operator authority so an agent still cannot walk back its own terminal. The table also now states that a status may only be set by the request that owns it, which is what closes the metadata-merge bypass. phase2a-shadow.md documents the authority split, the CLI's new non-zero exit on a refused write, and why the audit mirror compares transition_seq. Two reviewer observations are carried forward as 2b work: `started` does not yet verify the assigned session, and stop_session's unconditional cancel is now refused on an already-completed job, which is the table behaving as designed but worth a release note. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
This commit is contained in:
1 parent
56d08efa95
commit
3b134e3358
3 files changed
+23
-4
No files matched your search
@@ -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`(운영자 권한)을 쓴다. 릴리스 노트 기록 대상.
|
||||
@@ -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다.
|
||||
|
||||
@@ -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`과 일치**함을 단정한다.
|
||||
|
||||
|
||||
Reference in new issue
Block a user