Files
multi-agent-mux/docs/contracts/issue-5/phase2a-shadow.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

5.2 KiB

📑 Issue #5 Phase 2a — Shadow Watcher 운영 노트

  • 문서 버전: Rev.1
  • 상태: Phase 2a 완료 (shadow-only: 레지스트리 무기록)

1. 이 단계가 하는 일과 하지 않는 일

한다 하지 않는다
herdr agent wait로 에이전트 상태를 관측 관측으로 상태를 바꾸는 일
증거를 정책에 넣어 "완료 후보인가"를 판정 판정 결과를 레지스트리에 커밋
판정을 .mam/shadow/<job_id>.ndjson에 기록 job 레코드 필드를 추가/변경
4중 귀속 증거를 shadow 저장소에 보관 레코드 스키마 변경 (Phase 2b 결정)

자동완료는 Phase 2b다. 본 단계의 candidate_approved는 "커밋했을 것"이라는 로그일 뿐이다.

2. 운영 스위치

변수 기본 효과
MAM_ENGINE_CAS 1 0이면 update_job_status/pick_pending이 Phase 2a 이전의 보존된 본체로 분기한다. 재배포·revert 불필요.

Shadow 러너는 애초에 쓰기 경로가 없으므로 프로세스 종료만으로 흔적이 사라진다. .mam/shadow/는 레지스트리와 분리되어 있어 삭제해도 job 상태에 영향이 없다.

2-1. 권한 구분 (교차 리뷰 반영)

source 할 수 있는 것 할 수 없는 것
publish_event (에이전트) 자신의 작업을 running/completed/error/cancelled로 전이 terminal을 되돌리는 것 (redispatch/finalize 거부)
registry_cli (래퍼·운영자) 위 전부 + redispatch(terminal→pending, iteration 증가 필수) + finalize(terminal→error) iteration을 되돌리는 재생

CLI 종료 코드 변경: registry.py status/update가 CAS에 의해 거부되면 이제 stderr에 사유를 내고 non-zero(4) 로 종료한다. Phase 2a 이전에는 거부가 불가능해 항상 0이었고, 그 침묵이 반복 위임이 조용히 깨진 것을 감췄다.

3. 락 규율 (P2A-01)

with registry_lock:      판정 + 레코드 1회 쓰기 + 알림 큐잉      ← 블로킹 호출 금지
락 해제 후:              감사 로그 flush, 발행 등 느린 작업

감사 mirror가 락 밖으로 나가면서 순서 보장을 잃을 수 있으므로, 전이마다 단조 증가하는 transition_seq를 레코드와 알림에 함께 싣는다. update_logged_status는 기존 status.json의 transition_seq가 더 크거나 같으면 쓰기를 건너뛴다 — 지연된 flush가 최신 상태를 되돌리지 못하게 하는 것이며, 락이 공짜로 주던 순차 일관성을 복원한다(교차 리뷰 F-B). 이 형태는 새로 만든 것이 아니라 pick_pending이 이미 쓰던 패턴을 모든 전이로 일반화한 것이다.

4. herdr agent wait 사용 규칙

  • --timeout은 밀리초다. 코드의 파라미터명은 timeout_ms이며 초 단위 형태는 제공하지 않는다.
  • --until은 항상 명시한다. 생략 시 기본 매칭이 idle,done,blocked라 working이 빠진다.
  • exit 1의 페이로드는 stderr에 있다. stdout은 비어 있으므로 stdout에서 에러를 읽으려 하면 안 된다.
  • 타임아웃은 건강한 응답이다. 소켓이 답했고 상태가 안 바뀐 것뿐이다. 전송 실패와 구분해야 한다.
  • 전송 실패 시 백오프는 1→2→4→8→16초 상한이며, 캐시된 스냅샷을 폐기한다. 단절 구간 이전의 상태로 추론하면 단절이 완료처럼 보인다.

5. 판정 순서 (fail-closed)

  1. 전송 이상 → hold_reconnect (증거 자체가 무효)
  2. 다른 세대/다른 pane 신호 → reject_stale
  3. blocked → hold_blocked (사람 대기 중 완료 금지)
  4. 4중 귀속 + 실행 앵커 미충족 → hold
  5. 비-quiescent 또는 도구 실행 중 → hold
  6. 리포트 미확정(매니페스트/해시) → hold
  7. 전부 통과 → candidate_approved

6. 시나리오 계약

Phase 0의 7종 + Phase 2a의 5종 = 12종이 expected_policy.decision으로 판정을 고정한다. 신규 5종도 Phase 0의 validate_watcher_scenario를 통과해야 하므로 증거 스키마가 계속 강제된다.

7. Phase 2b 인계

  1. attempt_id 등 귀속 필드를 레코드로 승격할지 결정 (현재는 shadow 저장소).
  2. policy.decide의 candidate_approved를 engine.transition("completed", expected_attempt=...)에 연결. 엔진과 CAS 표는 이미 준비되어 있다.
  3. Pending Notification 지연 알림·크래시 복구 (state-transitions.md §5).
  4. preexec_fn: Phase 2a는 데몬을 스폰하지 않아 미노출. 2b가 스폰을 도입하면 스레드 안전한 방식으로 교체해야 한다.
  5. started의 세션 확인 (O-A): _decide의 claim/started는 아직 "작업 할당 세션 일치"를 검사하지 않는다(pick_pending은 세션 필터를 하지만 publish_event의 started는 누구나 보낼 수 있다). 2b에서 expected_session을 추가할 것.
  6. stop_session.sh의 무조건 cancelled (O-B): 이미 completed인 job에 대해 이제 거부된다(레거시는 덮어썼다). 표상 의도된 동작이며, 운영자가 확정 상태를 바꾸려면 registry.py status --set(운영자 권한)을 쓴다. 릴리스 노트 기록 대상.