
     j=                       d Z ddlmZ ddlZddlmZ ddlmZmZ ddlm	Z	 ddl
mZmZ 	 ddlZddlmZ d	Zd
ZddZddZ	 	 d	 	 	 	 	 ddZe G d d             Zdded	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZddd	 	 	 	 	 ddZ	 	 dddd	 	 	 	 	 	 	 	 	 	 	 ddZdded	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZy# e$ r dZY w xY w)u9  utils/fallback_schedule_registry.py
task-2728 — durable fallback schedule registry for idempotent prune.
fallback 등록 시 (task_id, round, head_sha, cron_id, registered_at, owner_key)를 durable JSONL에 기록.
collector가 이 registry를 읽어 (task_id·round·head) 단위로 pending fallback을 prune.
    )annotationsN)	dataclass)datetimetimezone)Path)CallableOptional)CANONICAL_ROOT_DEFAULTfallback_schedule_registry_v1z-memory/state/fallback_schedule_registry.jsonlc                    t        | dd      5 }t        3	 t        j                  |j                         t        j                         |j                  |       ddd       y# t
        $ r Y %w xY w# 1 sw Y   yxY w)ug   JSONL 한 줄 append. fcntl 지원 시 LOCK_EX 로 쓰기 race 방지, 미지원 시 graceful fallback.autf-8encodingN)openfcntlflockfilenoLOCK_EXOSErrorwrite)rplinefhs      Q/home/jay/workspace/.worktrees/task-2729-dev6/utils/fallback_schedule_registry.py_append_lockedr      sn    	b#	( BBIIK7 	   	 s.   A22A#	A2#	A/,A2.A//A22A;c                 f    t        j                  t        j                        j	                  d      S )Nz%Y-%m-%dT%H:%M:%SZ)r   nowr   utcstrftime     r   _now_utcr#   $   s!    <<%../CDDr"   c                P    |t        |      S t        | xs t              t        z  S )u   registry_path 주어지면 Path(registry_path),
    아니면 Path(canonical_root or CANONICAL_ROOT_DEFAULT)/_REL_PATH.
    autoset/cwd 추측 금지.
    )r   r
   	_REL_PATHcanonical_rootregistry_paths     r   resolve_registry_pathr)   (   s+      M""8"89IEEr"   c                      e Zd ZU ded<   ded<   ded<   ded<   ded<   ded<   d	Zded
<   dZded<   dZded<   ddZddZe	dd       Z
y)FallbackScheduleRecordstrtask_idintroundhead_shacron_idregistered_at	owner_keyPENDINGstatusNOptional[str]cause
updated_atc                ^    | j                   | j                  | j                  | j                  fS )N)r-   r/   r0   r1   selfs    r   keyzFallbackScheduleRecord.keyA   s!    djj$--FFr"   c                    t         | j                  | j                  | j                  | j                  | j
                  | j                  | j                  | j                  | j                  d
S )N)
schemar-   r/   r0   r1   r2   r3   r5   r7   r8   )
REGISTRY_SCHEMAr-   r/   r0   r1   r2   r3   r5   r7   r8   r:   s    r   to_dictzFallbackScheduleRecord.to_dictD   sP    %||ZZ||!//kkZZ//
 	
r"   c                     | |d   t        |d         |d   |d   |d   |j                  dd      |j                  dd	      |j                  d
      |j                  d      	      S )Nr-   r/   r0   r1   r2   r3    r5   r4   r7   r8   	r-   r/   r0   r1   r2   r3   r5   r7   r8   )r.   get)clsds     r   	from_dictz FallbackScheduleRecord.from_dictR   sj    iLaj/z]iLO,eeK,559-%%.uu\*

 
	
r"   )returntuple)rH   dict)rF   rJ   rH   z'FallbackScheduleRecord')__name__
__module____qualname____annotations__r5   r7   r8   r<   r@   classmethodrG   r!   r"   r   r+   r+   5   s^    LJMLNFCE= $J$G
 
 
r"   r+   )r'   r(   now_fnc                    t        | ||| |       |ddd	      }t        ||      }	|	j                  j                  dd       t	        |	t        j                  |j                         d      d	z          |S )
ut   PENDING 레코드 생성, JSONL 한 줄 append.
    파일 없으면 parent mkdir 후 생성. 레코드 반환.
    r4   NrC   r&   Tparentsexist_okFensure_ascii
r+   r)   parentmkdirr   jsondumpsr@   )
r-   r/   r0   r1   r3   r'   r(   rP   recr   s
             r   register_fallbackr^   a   sr     !h
C 
nM	ZBIIOOD4O02tzz#++-eDtKLJr"   r&   c                   t        | |      }|j                         sg S i }	 t        |dd      5 }|D ]S  }|j                         }|s	 t	        j
                  |      }t        j                  |      }|||j                         <   U 	 ddd       t        |j                               S # t        j                  t        t        t        f$ r Y w xY w# 1 sw Y   KxY w# t        $ r g cY S w xY w)u   JSONL 전체 읽어 (task_id, round, head_sha, cron_id) key별
    마지막 상태(append-only last-wins) 레코드 리스트 반환.
    파일 없으면 []. 파싱 불가 줄은 skip.
    r&   rr   r   N)r)   existsr   stripr[   loadsr+   rG   r<   JSONDecodeErrorKeyError	TypeError
ValueErrorr   listvalues)	r'   r(   r   seenr   rawr   rF   r]   s	            r   read_recordsrl      s     
nM	ZB99;	D"cG, 
	 	yy{

4(A0::1=C&)DO	
	 	 ,,h	:N 
	 
	  	sL   C! C=B,CC! ,#CCCCCC! !C/.C/c                   t        ||      }g }|D ]W  }|j                  dk7  r|j                  | k7  r#||j                  |k7  r5||j                  |k7  rG|j                  |       Y |S )ux   read_records 중 status=="PENDING" 이고 task_id 일치
    (+round/head_sha 주어지면 일치)하는 레코드.
    r&   r4   )rl   r5   r-   r/   r0   append)r-   r/   r0   r'   r(   recsresultr]   s           r   pending_forrq      s}     ~]SDF 	::";;'!e!3CLLH$<c	 Mr"   c                    t        | |||ddd| |       	      }t        ||      }	|	j                  j                  dd       t	        |	t        j                  |j                         d      d	z          |S )
u   status="PRUNED", cause, updated_at=now_fn() 인 tombstone 레코드 JSONL append, 반환.
    이미 PRUNED여도 재호출 안전(idempotent).
    rB   PRUNEDrC   r&   TrR   FrU   rW   rX   )
r-   r/   r0   r1   r7   r'   r(   rP   r]   r   s
             r   mark_prunedrt      sr     !8
C 
nM	ZBIIOOD4O02tzz#++-eDtKLJr"   )r   r   r   r,   rH   None)rH   r,   )NN)r'   r6   r(   r6   rH   r   )r-   r,   r/   r.   r0   r,   r1   r,   r3   r,   r'   r6   r(   r6   rP   Callable[[], str]rH   r+   )r'   r6   r(   r6   rH   rh   )r-   r,   r/   zOptional[int]r0   r6   r'   r6   r(   r6   rH   rh   )r-   r,   r/   r.   r0   r,   r1   r,   r7   r,   r'   r6   r(   r6   rP   rv   rH   r+   )__doc__
__future__r   r[   dataclassesr   r   r   pathlibr   typingr   r	   r   ImportErrorutils.callback_envelope_schemar
   r?   r%   r   r#   r)   r+   r^   rl   rq   rt   r!   r"   r   <module>r~      s  
 #  ! '  % B1;	E
 %)#'
F!
F 
F 

F (
 (
 (
d %)#' (  	
   " !  B %)#'! ! 
	@  "
 %)#' 
 " ! 
B %)#' (  	
   " !  Q  Es   B3 3B=<B=