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
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})
job_id: 등록된 고유 작업 식별자 (32-bit hex 또는 UUID).attempt_id: 단일 Job 내에서 재시도 시 단조 증가하는 시도 번호.agent_session_uuid: 에이전트 프로세스 기동 시 할당된 영속 고유 식별자.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다. — 교차 리뷰cb792e91F-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]
writing단계:- 에이전트가 임시 파일(
report-final.md.tmp)을 작성 중인 상태. - 워처는 이 상태에서 완료를 절대 인정하지 않음 (Hold).
- 에이전트가 임시 파일(
stable_candidate단계 (1차/보조 필터):- 리포트 파일의 mtime이 단조 시계(monotonic clock) 기준 최소 15초(기본 권장값) 동안 불변인 상태.
- ⚠️ 불변식: 디바운스 경과(15s)만으로는 완료 권한이 성립되지 않습니다. 이는 일시적인 LLM 생각 중단이나 네트워크 지연일 수 있으므로 오직 '후보 필터'로만 기능합니다.
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
트랜잭션 보장 규칙:
- 단일 내구성 작업 레코드:
- 독립된 별도의 Outbox DB를 신설하지 않고, 기존
.mam/jobs/<id>.json레코드 내에pending_notification: { event, timestamp, seq, signature }를 원자적으로 기록합니다.
- 독립된 별도의 Outbox DB를 신설하지 않고, 기존
- 락 해제 후 전송:
- 레지스트리 락을 해제한 후 비동기 워커가 MQTT 발행을 수행합니다.
- 크래시 복구(Crash Recovery):
- 엔진 재시작 시
status in TERMINAL이면서pending_notification이 남아있는 레코드를 스캔하여 멱등적으로 재전송합니다.
- 엔진 재시작 시
6. 권한 차단(Permission Blocked) 해제 정책
- 에이전트 터미널이
blocked(권한 승인 대기) 상태로 관측되면, 어떠한 경우에도 자동완료 전이를 시도하지 않습니다. - 차단 해제 판단:
- 묵시적 전이 금지: 단순히 다음 관측에서
working이 되었다고 해서 이전 차단이 정당하게 해결되었다고 단정하지 않습니다. - 명시적 승인 증거 또는 운영자의 승인 입력 로그가 확인되지 않는 한, 작업은
running (blocked)상태를 유지하며 데드라인 도달 시 안전하게 타임아웃 처리합니다.
- 묵시적 전이 금지: 단순히 다음 관측에서