
    j}j7                       d Z ddlmZ ddlZddlZddlZddlmZ ddlm	Z	 dZ
dZdZd	Zd
ZdZdZdZdZdZdZdZdZdZdZ ee      j5                         j6                  j6                  Z eh d      Zd#dZd$dZ d%d&dZ!efd'dZ"	 	 d(	 	 	 	 	 	 	 	 	 d)dZ#d*dZ$d Z%d%d+dZ&ddedddd	 	 	 	 	 	 	 	 	 	 	 	 	 d,dZ'd%d-d Z(g d!Z)e*d"k(  r e+ e(             y).u  dispatch.spawn_verification — dispatch 반환 직전 봇 spawn 검증 결선.

task-2942 (ANU 근본원인 260813) — dispatch false-OK 감지.

배경:
  dispatch() 는 `cokacdir --cron` 등록이 성공하면 status="dispatched" 를 반환하고
  **봇이 실제로 spawn 됐는지 검증하지 않았다**. `_verify_bot_spawn`
  (scripts/harness/v36/terminal_state_classifier.py §5.2.1) 은 구현돼 있었으나
  dispatch 실행 경로에 호출부가 0건이었다(미결선). 그래서 cron 전달 채널이
  간헐 silent-drop 하면 봇은 미spawn 인데 dispatch 는 여전히 ok = **false-OK**.

본 모듈의 역할 (1차 = 감지 + 표면화만):
  1) ``write_spawn_confirmed_marker`` — 봇 세션 시작 시 spawn-confirmed marker 생성.
     이 마커가 있어야 §5.2.1 이 SPAWNED 를 판정할 수 있다 (기존엔 마커 메커니즘
     자체가 미작동이었다).
  2) ``verify_and_annotate`` — dispatch 성공 반환 직전 spawn 검증을 호출하고,
     DISPATCH_FALSE_OK 를 result["status"] 에 명시 + handoff marker 를 disk 박제.

  ★ 자동 재전송(봇별 key fallback)은 본 범위가 아니다(무한루프/중복 위험 → 2차).
    이번엔 "조용히 ok 로 넘어가는 것"만 차단한다.

무손상 원칙 (비차단):
  검증은 dispatch 를 절대 죽이면 안 된다. import 실패(`_verify_bot_spawn is None`),
  예외, 비정상 입력(result 가 dict 아님) 시 **원래 result 를 그대로 반환**한다.
  긴급 시 ``DISPATCH_SPAWN_VERIFY_ENABLED=0`` 으로 코드 변경 없이 무력화 가능하다.

본 모듈은 stdlib 만 import 한다 (봇 시작 CLI 가 무거운 dispatch 패키지를 로드하지
않도록, 파일 직접 실행 `python3 dispatch/spawn_verification.py` 를 지원한다).
    )annotationsN)Path)OptionalSPAWNEDDISPATCH_FALSE_OKTIMEOUT_BOT_ALIVE_BUT_NO_MARKERspawn_verificationspawn_verification_reasonspawn_verification_warningspawn_false_okINFRA_DEFECTDISPATCH_SPAWN_VERIFY_ENABLEDDISABLED'FAILURE_CALLBACK_2712_SPAWN_TIMEOUT_SECDISPATCH_SPAWN_VERIFY_WAIT_SEC   
    FAILURE_CALLBACK_2712_EVENTS_DIR>    0noofffalsec                     t         j                  j                  t              } | y| j	                         j                         t        vS )uV   spawn 검증 활성 여부. 기본 ON, ``DISPATCH_SPAWN_VERIFY_ENABLED=0`` 이면 OFF.T)osenvirongetENABLED_ENVstriplower_FALSEY)raws    dispatch/spawn_verification.py
is_enabledr$   O   s6    
**..
%C
{99;g--    c                 p    t         j                  j                  t        t	        t
        dz  dz              S )uM   marker 를 읽고/쓰는 events 디렉토리 (dispatch.py 와 동일 규칙).memoryevents)r   r   r   EVENTS_DIR_ENVstr
_REPO_ROOT r%   r#   default_events_dirr-   W   s&    ::>>.#j8.Ch.N*OPPr%   c                    t         j                  j                  |       }||S 	 t        t	        |      j                               S # t        t        f$ r |cY S w xY w)uh   env 정수 파싱. 미설정/파싱불가 시 default (검증이 죽으면 안 되므로 예외 금지).)r   r   r   intr*   r   	TypeError
ValueError)namedefaultr"   s      r#   _env_intr4   \   sR    
**..
C
{3s8>>#$$z" s   !A AAc                    t        t              }||dk\  r|S t        t        t              }||dk  rt        }	 t	        dt        |             }||z   S # t        t        f$ r t        }Y ||z   S w xY w)u   총 marker 대기 초.

    우선순위: DISPATCH_SPAWN_VERIFY_WAIT_SEC > (dispatch_delay_sec + 마커 예산).
    마커 예산은 FAILURE_CALLBACK_2712_SPAWN_TIMEOUT_SEC (기본 15s) 를 그대로 쓴다.
    r   )	r4   TOTAL_WAIT_ENVMARKER_TIMEOUT_ENVDEFAULT_MARKER_TIMEOUT_SECmaxr/   r0   r1   DEFAULT_DISPATCH_DELAY_SEC)dispatch_delay_secoverridemarker_budgetdelays       r#   resolve_wait_secr?   g   s     'HA/1KLM 12+As-./ =   z" +*=  +s   A A10A1c                   | syt        |xs
 t                     }t        j                  dt        j                               }| |xs d|t        |      nt        j                         |d}	 |j                  dd       ||  d| dz  }|j                  t        j                  |d	d
      dz   d       t        |      S # t        $ r Y yw xY w)ux  ``{events_dir}/{task_id}.spawn-confirmed-<ts>.json`` 을 생성한다.

    봇 세션이 시작될 때 첫 명령으로 호출된다. 이 마커가 있어야 §5.2.1
    ``_verify_bot_spawn`` 이 SPAWNED 를 판정한다.

    최소 내용: task_id / bot_id / pid / ts.

    Returns:
        생성된 marker 경로. 실패 시 None (봇 작업을 죽이지 않는다).
    Nz%Y%m%dT%H%M%Sunknown)task_idbot_idpidtsT)parentsexist_okz.spawn-confirmed-z.jsonF   )ensure_asciiindent
zutf-8)encoding)r   r-   timestrftimegmtimer/   r   getpidmkdir
write_textjsondumpsr*   	Exception)rB   rC   rD   
events_dir
target_dirrE   payloadpaths           r#   write_spawn_confirmed_markerrZ   }   s      j8$6$89J		6B%I?s3x			G	5wi'8EBBJJwU1=D 	 	
 4y s   .AC 	CCc                    t        | t              r|| S || t        <   |t        k(  rt        | d<   t        | t
        <   | S |t        k(  rt        | t        <   | S )u  dispatch 반환 dict 에 spawn 검증 판정을 기록한다 (같은 dict 반환).

    - SPAWNED → 판정만 기록, status 유지 (정상 경로 무손상).
    - DISPATCH_FALSE_OK → status 를 DISPATCH_FALSE_OK 로 명시 + 사유 기록.
    - TIMEOUT_BOT_ALIVE_BUT_NO_MARKER → 경고 필드만 기록, status 는 유지
      (봇은 살아있으므로 false-OK 가 아니다).
    - verdict 가 None(검증 불가) → 원본 무손상.
    status)
isinstancedictSPAWN_VERIFICATION_KEYr   SPAWN_FALSE_OK_FAILURE_KINDSPAWN_VERIFICATION_REASON_KEYr   SPAWN_VERIFICATION_WARNING_KEY)resultverdicts     r#   annotate_spawn_verificationre      sb     fd#w%,F!"##,x0K,- M 
3	31P-.Mr%   c                     	 ddl m}  | S # t        $ r Y nw xY w	 ddl}t	        t
        dz  dz  dz        }||j                  vr|j                  j                  d|       ddl m}  | S # t        $ r Y yw xY w)uE   ``_verify_bot_spawn`` 을 지연 import. 실패 시 None (비차단).r   )_verify_bot_spawnNscriptsharnessv36)terminal_state_classifierrg   rU   sysr*   r+   rY   insert)rg   rl   ri   s      r#   _load_verify_fnrn      s|    ?   
j9,y85@A#(("HHOOAw'?   s   
 	AA( (	A43A4c                   |}|O	 ddl }t        t        dz  dz  dz        }||j                  vr|j                  j	                  d|       ddlm} |}	  || t        t        |      S # t        $ r Y yw xY w# t        $ r Y yw xY w)uG   false-OK 를 disk handoff marker 로 박제 (best-effort · 비차단).Nr   rh   ri   rj   )write_handoff_marker)failure_kindrV   )
rl   r*   r+   rY   rm   failure_callback_dispatcherrp   rU   SPAWN_FALSE_OK_TERMINAL_STATEr`   )rB   rV   
handoff_fnfnrl   ri   rp   s          r#   _emit_false_ok_handoffrv      s    	B	z
	*y09<uDEGchh&7+H%B)4!	
 	
  		  s$   AA) A8 )	A54A58	BB)	child_pidrV   r;   	verify_fnrt   loggerc          	     t   t        | t              s| S t               st        | t        <   | S ||n	t               }	|	|	 |j                  d       | S | S |xs
 t               }
	  |	||||
t        |            }t        | |      } |t        k(  r8t        ||
|      }|r|| d<   |	 |j                  d| d	t         d
       | S | S |t        k(  r!|	 |j                  d| d	t         d       | S | S # t        $ r Y | S w xY w# t        $ rR 	  |	||||
      }nA# t        $ r5}|%	 |j                  d|        n# t        $ r Y nw xY w| cY d}~cY S d}~ww xY wY t        $ r3}|%	 |j                  d|        n# t        $ r Y nw xY w| cY d}~S d}~ww xY w# t        $ r Y | S w xY w# t        $ r Y | S w xY w)u  dispatch 성공 반환 직전 spawn 검증 결선 진입점 (비차단).

    cron 방식은 child_pid 가 없으므로 marker 기반으로만 판정한다.

    ★ 무손상 계약: 아래 어떤 경우에도 원래 result 를 그대로 돌려준다.
      - result 가 dict 가 아님
      - kill-switch OFF (``DISPATCH_SPAWN_VERIFY_ENABLED=0``)
      - ``_verify_bot_spawn`` import 실패 (None)
      - 검증 중 예외 발생
    NuH   [spawn-verify] _verify_bot_spawn 미탑재 — 검증 스킵 (무손상))rV   timeout_sec)rV   u'   [spawn-verify] 검증 실패 (무시): )rt   spawn_verification_handoffz[spawn-verify] z: uo    — cron 등록은 OK 였으나 spawn-confirmed marker 미확인 (봇 미spawn 의심). handoff marker 박제.u<    — 봇은 살아있으나 marker 미생성 (status 유지))r]   r^   r$   VERIFY_DISABLEDr_   rn   warningrU   r-   r?   r0   re   r   rv   errorr   )rc   rB   expected_bot_idrw   rV   r;   rx   rt   ry   ru   _events_dirrd   excmarkers                 r#   verify_and_annotater      sG   , fd#<)8%&+1BB	zij v4 2 4K"();<
4 )9F##'T39F/0%gYb1B0C De f M6M 
3	38J	NN!'"-L,M NJ K M6Mo    
	/9UG 	!NN%LSE#RS  M	   !HNO (   M  	M	s   C2 #D &F F* 2	C?>C?	FDF	E"E%D:9E:	EEEE
EFEFF$F'E<;F<	FFFFFF	F'&F'*	F76F7c                   dd l }|j                  d      }|j                  dd       |j                  dd	       |j                  d
t        d        |j                  dd 	       |j	                  |       }t        |j                  |j                  |j                  |j                        }t        t        j                  |rdnd||j                  dd             |rdS dS )Nr   u5   spawn-confirmed marker 생성 (봇 세션 시작 시))descriptionz	--task-idT)requiredz--bot-idrA   )r3   z--pid)typer3   z--events-dir)rD   rV   okr   )r\   r   rB   F)rI      )argparseArgumentParseradd_argumentr/   
parse_argsrZ   rB   rC   rD   rV   printrS   rT   )argvr   parserargsrY   s        r#   _mainr   L  s    $$1h$iF
d3

I6
c48
5T"D'dkktxxDOOD 


#tDT\\Z	
 1!r%   )r   r   r   r_   r`   r   r$   r-   r?   rZ   re   r   __main__)returnbool)r   r*   )N)r2   r*   r3   Optional[int]r   r   )r;   r/   r   r/   )NN)
rB   r*   rC   r*   rD   r   rV   Optional[str]r   r   )rc   r^   rd   r   r   r^   )rB   r*   rV   r*   r   zOptional[dict])rc   r^   rB   r*   r   r*   rw   r   rV   r   r;   r/   r   r^   )r   r/   ),__doc__
__future__r   rS   r   rM   pathlibr   typingr   r   r   r   r_   ra   rb   r`   rs   r   r}   r7   r6   r8   r:   r)   __file__resolveparentr+   	frozensetr!   r$   r-   r4   r?   rZ   re   rn   rv   r   r   __all____name__
SystemExitr,   r%   r#   <module>r      s  : #  	   
 ' "C  .  ; !=  /  .  . ? 1  3(^##%,,33

3
4.Q
 0J !2  $	### 
# 	#
 #R.*>  $ $8ZZZ Z
 Z Z Z 
Z@,  z
UW
 r%   