Files
multi-agent-mux/docs/contracts/issue-5/state-transitions.md
T
GodopuandClaude Opus 5 0e74aa1e4e docs(issue-5): record the evidence requirement, the audit lock and the ack rule
Three contract points the round 2 fixes established.

An idempotent re-receipt is an ACK in the table, so a caller must treat it as
success. Only a genuine refusal is non-zero, and an invalid status stays the
usage error it always was. That distinction is what keeps the wrapper's approved
path from aborting under set -e.

Completion attribution now requires evidence to be present, not merely
non-contradictory, and names where it may be read from: a collector envelope
beside the payload, or a non-native source's own payload. Never from inside a
native Herdr payload, because MAM fields are forbidden there and one appearing
anyway is not evidence of anything.

The audit mirror's ordering guard is documented as a locked read-compare-write,
with the reason a bare sequence number was not enough, and the note that the lock
is deliberately taken outside the registry transaction.

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

11 KiB

📑 Target State Transitions and CAS Specification

  • Document Version: Rev.5 (멱등 ACK 구분 및 관측 증거 필수 요건 명문화)
  • Issue: Gitea Issue #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}에서만 허용된다. 에이전트(publish_event)는 자신의 terminal을 되돌릴 수 없다 — 이것이 CAS 표의 존재 이유이며, 위 두 행은 그 원칙의 예외가 아니라 래퍼가 수행하는 별개 전이를 이름 붙인 것이다.

🔑 멱등 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) 트랜잭션을 명세합니다:

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) 상태를 유지하며 데드라인 도달 시 안전하게 타임아웃 처리합니다.