Files
multi-agent-mux/docs/contracts/issue-5/state-transitions.md
T
GodopuandClaude Opus 5 17a2dcd988 docs(issue-5): phase 2a contracts, scenarios and task checklist
Records what phase 2a actually delivers and, as importantly, what it does not:
the shadow runner decides and logs, and nothing commits an inferred completion
until 2b.

state-transitions.md gains an implementation mapping so each contract row names
the code and the test that hold it up, with the inferred-completed row marked as
judged-but-not-committed.

phase2a-shadow.md is the operations note: the rollback switch, the lock
discipline and why transition_seq exists, the herdr wait rules that are easy to
get wrong (milliseconds, always name --until, errors on stderr, a timeout is a
healthy answer), the fail-closed decision order, and the 2b handoff.

task.md records P2A-00..04 with commit evidence, including two corrections to my
own plan: pick_pending was already a correct swap rather than a hole, and the
engine had to live with the registry writers because putting it in lib_py/loop
would have made delegation import a sibling and mix trees under a frozen loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrDNJkHmm18uu7n7JwV5Qd
2026-09-06 18:58:01 +09:00

135 lines
9.4 KiB
Markdown

# 📑 Target State Transitions and CAS Specification
- **Document Version**: Rev.3 (Phase 2a 구현 매핑 추가)
- **Issue**: [Gitea Issue #5](https://git.godopu.com/tmpl/multi-agent-mux/issues/5)
- **Scope**: Target Compare-And-Swap (CAS) State Machine & Attribution Contract
---
## 1. 이벤트(Event)와 상태(Status)의 개념적 분리
기존 시스템의 혼선을 방지하기 위해 용어를 명확하게 분리하여 정의합니다:
- **이벤트 (Event)**: 에이전트, 워처(Watcher), 또는 외부 운영자가 특정 시점에 메시징 백플레인(MQTT)이나 로컬 채널로 전달하는 **사건(Fact)**입니다 (`started`, `progress`, `permission_required`, `completed`, `error`, `cancelled`).
- **상태 (Status)**: 단일 상태 제어 엔진(Engine)이 원자적 CAS 트랜잭션을 통해 공통 레지스트리에 영속적으로 기록하는 **현재 작업의 공식 생애주기 단계**입니다 (`pending`, `running`, `completed`, `error`, `cancelled`).
> 📌 **핵심 원칙**: 이벤트는 상태를 변경해 달라는 **전이 요청(Transition Request)**일 뿐이며, 엔진의 유효성 검증과 CAS 게이트를 통과하기 전까지는 레지스트리 상태가 변경되지 않습니다.
---
## 2. 4중 실행 귀속(Execution Attribution) 보장 계약
단순히 에이전트 터미널이 유휴(`idle`) 상태라고 해서 작업이 성공적으로 종료되었다고 추론(Inferred Completion)해서는 안 됩니다. 이전 작업의 잔여 유휴 상태, 세션 재사용, 또는 프로세스 재시작으로 인한 오탐을 원천 방지하기 위해 다음 **4중 식별자 튜플**을 필수로 검증합니다:
$$\text{Attribution Tuple} = (\text{job\_id}, \text{attempt\_id}, \text{agent\_session\_uuid}, \text{pane\_process\_generation})$$
1. **`job_id`**: 등록된 고유 작업 식별자 (32-bit hex 또는 UUID).
2. **`attempt_id`**: 단일 Job 내에서 재시도 시 단조 증가하는 시도 번호.
3. **`agent_session_uuid`**: 에이전트 프로세스 기동 시 할당된 영속 고유 식별자.
4. **`pane_process_generation`**: TMUX/Herdr 창 및 기저 셸 프로세스의 시작 시각(`lstart`) 및 PID 조합.
### 귀속 및 실행 앵커 불변식:
- 동일 에이전트 세션에는 동시에 단 하나의 활성 작업(`job_id`)만 배정됩니다.
- 디스패치 확인(`dispatch_ack`) 이후, 해당 `attempt_id`에 속하는 실제 터미널 출력 및 활동(Execution Anchor)이 관측된 경우에만 완료 후보로 인정합니다.
- 네이티브 API가 제공하지 못하는 식별자는 `null + reason`으로 표기하며, 가짜 값을 임의로 주입하지 않습니다.
---
## 3. 목표 CAS 상태 전이표 (Target State Machine)
모든 상태 변경은 Registry Lock 보호 하에서 기대 상태(`expected_state`), 시도 세대(`attempt_id`), 버전 번호(`version`)를 원자적으로 비교한 후 갱신(CAS)합니다.
| 현재 상태 (`Current`) | 요청 신호 (`Request`) | 전이 조건 및 검증 규칙 | 결과 상태 (`Next`) | 비고 및 갈등 처리 |
|---|---|---|---|---|
| `pending` | `started` / `claim` | 작업 할당 세션 및 `attempt_id` 일치 확인 | `running` | `started`는 상태 enum이 아니며 `running`으로 전이 |
| `running` | `progress` | 비-Terminal 유지. 최근 활동 시각 및 세부 정보 갱신 | `running` | 상태 변경 없이 메타데이터만 갱신 |
| `running` | `permission_required` | 비-Terminal 유지. `blocked_reason` 기록 | `running` | 권한 대기 중에는 자동완료 전면 금지 |
| `running` | `explicit terminal` | 유효한 작업자 세션의 서명된 `completed`/`error`/`cancelled` 수신 | 요청된 Terminal | 정상 종료 커밋 |
| `running` | `inferred completed` | 4중 귀속 일치 + 실행 앵커 확인 + 확정 해시/매니페스트 존재 + Fresh `idle/done` + 블로커 없음 + 부수효과 검증 | `completed` | Fail-Closed AND 게이트 통과 시에만 CAS 커밋 |
| `terminal` | 동일 논리적 완료 재수신 | 기 커밋된 Terminal 상태와 동일한 요청 | `terminal` (No-op) | 멱등(Idempotent) ACK 반환, 추가 디스패치 금지 |
| `terminal` | 상충 신호 (`progress`/다른 Terminal) | 이미 Terminal 상태에 도달한 후 늦게 도착한 신호 | 기 커밋 상태 유지 | 상태 역행 및 덮어쓰기 거부, Conflict Audit Log 기록 |
| 임의 상태 | Stale Attempt / Generation | 과거 시도 세대 또는 프로세스 세대 불일치 신호 | 전이 거부 | Stale Attempt 경고 기록 및 패킷 폐기 |
| `running` | `deadline_timeout` | 작업 타임아웃 경과 및 실행 중단 확인 | `error` | 타임아웃 원인 기록 |
---
### 3-1. 구현 매핑 (Phase 2a)
| 계약 | 구현 | 테스트 |
|---|---|---|
| §3 CAS 전이표 | `lib_py/delegation/engine.py::_decide` (순수 함수, 락 안에서 호출) | `test_engine_*` |
| 단일 전이 주체 | `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_*` |
| 롤백 | `MAM_ENGINE_CAS=0` → 보존된 `_*_legacy` 본체 | 전 특성화 테스트 parametrize |
> 📌 **Phase 2a는 추론 완료를 커밋하지 않는다.** `inferred completed` 행은 `lib_py/loop/policy.py`가 `candidate_approved`로 **판정만** 하고, shadow 러너가 이를 로그로 남긴다. 실제 CAS 커밋 연동은 Phase 2b다.
---
## 4. 리포트 확정 단계 (Report Finalization Lifecycle)
자동완료 추론 시 파일 쓰기 도중 발생하는 불완전 리포트 채택을 방지하기 위해 3단계 확정 계약을 수립합니다:
```
[writing] ──(쓰기 진행 중)──> [stable_candidate] ──(원자적 교체 및 매니페스트)──> [finalized]
```
1. **`writing` 단계**:
- 에이전트가 임시 파일(`report-final.md.tmp`)을 작성 중인 상태.
- 워처는 이 상태에서 완료를 절대 인정하지 않음 (Hold).
2. **`stable_candidate` 단계 (1차/보조 필터)**:
- 리포트 파일의 mtime이 단조 시계(monotonic clock) 기준 **최소 15초(기본 권장값)** 동안 불변인 상태.
- ⚠️ **불변식**: **디바운스 경과(15s)만으로는 완료 권한이 성립되지 않습니다.** 이는 일시적인 LLM 생각 중단이나 네트워크 지연일 수 있으므로 오직 '후보 필터'로만 기능합니다.
3. **`finalized` 단계 (2차/확정 계약)**:
- 최종 대상 경로(`report-final.md`)로의 **원자적 이름 변경(Atomic Rename)** 완료.
- 해당 시도의 `attempt_id`와 `report SHA-256 hash`가 명시된 **완료 매니페스트(Commit Manifest)** 생성 완료.
- 외부 부수효과(Git push, API 호출)가 수반되는 작업인 경우, 해당 부수효과의 완료 증거가 매니페스트에 첨부되어야 함.
- 리포트가 재작성되면 이전 해시는 즉시 무효화되며 새로운 확정 절차가 필요함.
---
## 5. 지연 알림 (Pending Notification) 및 트랜잭션 복구 설계
상태 전이 락(Registry Lock) 하에서 외부 I/O(MQTT 브로커 전송, 네트워크 호출)를 수행하면 데드락과 심각한 지연이 발생할 수 있습니다. 이를 방지하기 위해 다음 **내구성 지연 알림(Pending Notification) 트랜잭션**을 명세합니다:
```mermaid
sequenceDiagram
participant Engine
participant Registry as Job Registry (Disk)
participant Publisher as Async Publisher (Wire)
participant Broker as MQTT Broker
Note over Engine: 1. CAS 전이 결정
activate Engine
Engine->>Registry: 락 획득 (fcntl.flock)
Engine->>Registry: 원자적 상태 커밋 (status=completed, pending_notification=intent)
Engine->>Registry: 락 즉시 해제
deactivate Engine
Note over Publisher: 2. 비동기 알림 전송
Publisher->>Broker: MQTT 이벤트 발행 (HMAC 서명 포함)
alt 전송 성공
Broker-->>Publisher: ACK (PUBACK)
Publisher->>Registry: pending_notification 완료 표시
else 전송 실패 / 크래시
Note over Publisher: 백오프 재시도 및 기동 시 미완료 알림 복구 전송
end
```
### 트랜잭션 보장 규칙:
1. **단일 내구성 작업 레코드**:
- 독립된 별도의 Outbox DB를 신설하지 않고, 기존 `.mam/jobs/<id>.json` 레코드 내에 `pending_notification: { event, timestamp, seq, signature }`를 원자적으로 기록합니다.
2. **락 해제 후 전송**:
- 레지스트리 락을 해제한 후 비동기 워커가 MQTT 발행을 수행합니다.
3. **크래시 복구(Crash Recovery)**:
- 엔진 재시작 시 `status in TERMINAL`이면서 `pending_notification`이 남아있는 레코드를 스캔하여 멱등적으로 재전송합니다.
---
## 6. 권한 차단(Permission Blocked) 해제 정책
- 에이전트 터미널이 `blocked` (권한 승인 대기) 상태로 관측되면, **어떠한 경우에도 자동완료 전이를 시도하지 않습니다.**
- 차단 해제 판단:
- 묵시적 전이 금지: 단순히 다음 관측에서 `working`이 되었다고 해서 이전 차단이 정당하게 해결되었다고 단정하지 않습니다.
- 명시적 승인 증거 또는 운영자의 승인 입력 로그가 확인되지 않는 한, 작업은 `running (blocked)` 상태를 유지하며 데드라인 도달 시 안전하게 타임아웃 처리합니다.