Files
Godopu ec61511699 fix(issue-6): fail-closed registry timeout and TUI turn handshake
- Introduce finalize_unfinished in multi-agent-mux-delegate-job for fail-closed atomic transitions
- Enforce CAS dispatch_token and attempt_id pinning across engine, CLI, and lifecycle scripts
- Integrate Herdr wait_for_agent_idle turn handshake to prevent prompt loss and race conditions
- Resolve bash 3.2 set -u empty array expansion compatibility in lib.sh
- Preserve Issue #5 R2 contract in CAS transition engine (engine.py, publish_event.py, service.py)
- Add comprehensive regression suites (test_issue6_fail_closed.py, test_issue6_turn_handshake.py)
- Bump lockstep framework and skills to v4.4.1 across lib.sh, eager.py, VERSIONS.md, 8 SKILL.md
- Synchronize documentation (IMPROVEMENTS.md, MESSAGING.md, state-transitions.md, task.md, phase5-e2e.md)
- Unanimous PASS achieved across 4 reviewer team leaders (Claude, Codex, OpenCode, Grok)

Closes #6
2026-09-09 14:02:47 +09:00

145 lines
12 KiB
Markdown

# 📑 Target State Transitions and CAS Specification
- **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
---
## 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` | 타임아웃 원인 기록 |
| `terminal` | `redispatch` (**운영자 권한**) | 반복 위임(`--type loop\|discuss`)의 다음 회차 전달. `iteration`이 기존 값보다 **엄격히 증가**해야 함 | `pending` | 교차 리뷰 `68abd0ff` 보완. 재생(replay)은 iteration 미증가로 거부됨 |
| `terminal` | `finalize` (**운영자 권한**) | 반복 예산 소진 시 워크플로 확정 | `error` | 교차 리뷰 `68abd0ff` 보완 |
> 🔑 **권한 축(Authority)**: `redispatch`·`finalize`는 `source ∈ {registry_cli, operator, delegate_wrapper}`에서만 허용된다. 에이전트(`publish_event`)는 **자신의 terminal을 되돌릴 수 없다** — 이것이 CAS 표의 존재 이유이며, 위 두 행은 그 원칙의 예외가 아니라 래퍼가 수행하는 별개 전이를 이름 붙인 것이다.
>
> 🔑 **시도 정체성은 레코드에 있다**: `dispatch_token`은 시도를 시작하는 요청(`claim`/`redispatch`)에서 새로 발급되며, `expected_dispatch_token` 전제조건으로 **락 안에서** 대조된다. 커밋 전에만 확인하는 귀속 검사는 그 사이의 재디스패치를 막지 못한다 — 전제조건이 커밋과 원자적이려면 트랜잭션이 자기 락 안에서 읽을 수 있어야 한다. `iteration`(루프 회차)과는 다른 개념이므로 별도 필드다. 재위임된 작업(`attempt_id > 1`)의 비-운영자 소스 terminal 요청은 반드시 고정된 dispatch token 또는 `expected_attempt` 전제조건을 증명해야 커밋된다. — 교차 리뷰 `a45e5b7f` R2, Issue #6
>
> 🔑 **멱등 ACK는 거부가 아니다**: `terminal` 상태에서 같은 terminal을 재수신하면 표는 이를 **No-op ACK**로 정의한다. 호출자(운영자 CLI)는 이를 성공(rc 0)으로 취급해야 한다. 실제 거부(상충 신호·권한 부족)만 non-zero다. — 교차 리뷰 `cb792e91` F-C
>
> 🔑 **status는 요청으로만 설정된다**: 임의 필드로 실린 `status`는 경계 내부에서 거부된다(`progress`가 terminal을 커밋하는 우회 차단). 알 수 없는 요청명도 거부된다. `pending`도 소유 요청(`redispatch`)을 갖는다.
---
### 3-1. 구현 매핑 (Phase 2a)
| 계약 | 구현 | 테스트 |
|---|---|---|
| §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` + `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다.
---
## 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)` 상태를 유지하며 데드라인 도달 시 안전하게 타임아웃 처리합니다.