Files
multi-agent-mux/docs/contracts/issue-5/state-transitions.md
T
GodopuandClaude Opus 5 3b134e3358 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
2026-09-06 19:34:20 +09:00

11 KiB

📑 Target State Transitions and CAS Specification

  • Document Version: Rev.4 (운영자 권한 전이 2행 및 권한 축 추가 — 교차 리뷰 반영)
  • 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 표의 존재 이유이며, 위 두 행은 그 원칙의 예외가 아니라 래퍼가 수행하는 별개 전이를 이름 붙인 것이다.

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