
    -ۯj,                    |   d Z ddlmZ ddlZddlZddlmZmZ ddlm	Z	m
Z
mZmZ dZdZdZd	Zd
ZdZdZe G d d             Ze G d d             Z ed       G d d             Z e       Zd!dZd"dZ G d d      Z ej6                         Zd#dZd$dZd%dZ G d d      Z dddd	 	 	 	 	 	 	 	 	 	 	 	 	 d&d Z!y)'u  발송 장부 — **호출 경계**에서 시도를 적는다 (2026-09-20).

## 왜 따로 두는가

지금까지 지출은 「record 묶음이 움직였나」로 쟀다. 그것은 **기록 변경**
감지이지 **발송**의 증거가 아니다. 그리고 기존 로그를 사후에 세는 것도
근거가 못 된다:

  · `generate_image` 함수 입구 **1회 ≠ 실제 전송 1회** — 재시도 루프가
    함수 **안**에 있다
  · Router 에도 자체 `num_retries` 가 있다
  · ★**「로그 행 없음 = 전송 0」이 아니다** — 기록 실패를 삼키는 자리가
    있고, 재시도 `continue` 가 실패 로그보다 **앞**이다

그래서 **보내기 직전**에 적는다.

★**이번 범위는 메모리 장부 + 집계 보고까지다.** `attempt_id` 는 지금
 메모리에만 있고 로그·Opik 으로 **나가지 않는다** — 「같은 attempt id 로
 잇는다」는 **앞으로의 계약**이지 지금 된 것이 아니다. 잇게 되더라도 두
 기록을 **합산하지 않는다**(같은 발송을 두 번 센다).

## 관측 단위를 **스스로 밝힌다**

경계마다 볼 수 있는 것이 다르다. 그래서 시도마다 `granularity` 를 적는다:

    request_attempt  원시 요청을 **보내려 한** 자리 — 재시도 루프 **안**.
                     ★「물리 전송」이라고 부르지 않는다 — 이 지점 뒤의
                      DNS·연결 실패도 여기 세어진다(보냈는지 모른다).
    slot_entry       키 슬롯·Router 진입 — 그 **아래**에 자체 재시도가 있다
    sdk_entry        SDK 진입 — 내부 재시도는 안 보인다

★어느 단위도 **「물리 전송 총수」가 아니다**. 그래서 집계는 단위별로
 갈라서 돌려주고, 합산 키를 **만들지 않는다**.

## 이 장부는 **제동이 아니다**

보고값이다. 기존 예산·상한·래치는 그대로 둔다 — 아직 못 재는 경로를
0 으로 치고 제동을 푸는 것이 이 판의 병이었다.
    )annotationsN)	dataclassfield)AnyDictListOptionalrequest_attempt
slot_entry	sdk_entrysentokfailedunknownc                      e Zd ZU dZ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
<   eZ	ded<   dZ
ded<   ddZdddZdddZy)SendAttemptu   발송 시도 **한 번**.

    ★`attempt_id` 는 지금 **메모리에만** 있다 — 접수 확인·결과를 잇는 것은
     **앞으로의 감사 연결**이지 지금 된 것이 아니다.
    str
attempt_idkindgranularitysource modelworkvisitstatedetailc                    t         | _        y N)STATE_OKr   selfs    J/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/send_ledger.pyr   zSendAttempt.okO   s	    
    c                @    t         | _        t        |      d d | _        y )N  )STATE_FAILEDr   r   r   r"   r   s     r#   r   zSendAttempt.failedR   s    !
&k$3'r$   c                @    t         | _        t        |      dd | _        y)uH   ★**돈이 나갔는지 모른다** — 실패로 적으면 안 된다.Nr&   )STATE_UNKNOWNr   r   r   r(   s     r#   r   zSendAttempt.unknownV   s    "
&k$3'r$   N)returnNone)r   )r   r   r+   r,   )__name__
__module____qualname____doc____annotations__r   r   r   
STATE_SENTr   r   r   r   r    r$   r#   r   r   ;   s^     O IKE3OD#NE3OE3FC((r$   r   c                      e Zd ZU dZ ee      Zded<    eej                  d      Z
ded<   d	d	d	d
	 	 	 	 	 	 	 	 	 	 	 	 	 ddZddZddZy)
SendLedgeru3   한 걷기(또는 한 샷)의 발송 시도 모음.)default_factoryzList[SendAttempt]attemptsF)r6   reprr   _lockr   r   r   r   c               *   t        t        j                         j                  |||t	        |xs d      t	        |xs d      t	        |xs d            }| j
                  5  | j                  j                  |       d d d        |S # 1 sw Y   |S xY w)Nr   )r   r   r   r   r   r   r   )r   uuiduuid4hexr   r9   r7   append)r"   r   r   r   r   r   r   atts           r#   recordzSendLedger.recordc   sv     zz|''d#Fekr"TZRekr"	
 ZZMM  % 
 
s   "BBc                   i }| j                   5  t        | j                        }ddd       D ]  }|j                   d|j                   }|j                  |t        dt        dt        dt        di      }|t        xx   dz  cc<   |j                  t        k7  sm|j                  |j                  d      dz   ||j                  <    |S # 1 sw Y   xY w)u[   **단위별로 갈라** 센다 — 합산해서 「전송 총수」라고 하지 않는다.N/r      )r9   listr7   r   r   
setdefaultr2   r    r'   r*   r   get)r"   outrowsakeybuckets         r#   countszSendLedger.countsq   s    )+ZZ&D AVVHAamm_-C^^j!Xq"A}a9:F :!#ww*$"(**QWWa"81"<qww  
 Zs   CCc                *   g }t        | j                               D ]e  }| j                         |   }| d|t            }|t           r|d|t            z  }|t           r|d|t            dz  }|j                  |       g dj                  |      S )Nu    발송 u	   ·실패 u   ·**결과불명 z**z / )sortedrM   r2   r'   r*   r?   join)r"   partsrK   csegs        r#   summaryzSendLedger.summary   s    $++-(Cc"AE!J-1C1\?"344*1]+;*<B??LL ) zz%  r$   N)r   r   r   r   r   r   r   r   r   r   r   r   r+   r   )r+   zDict[str, Dict[str, int]])r+   r   )r-   r.   r/   r0   r   rE   r7   r1   	threadingLockr9   rA   rM   rT   r3   r$   r#   r5   r5   \   s|    ="'"=H=y~~EBE3B Rb),69"03 

!r$   r5   T)frozenc                  .    e Zd ZU dZdZded<   dZded<   y)SendContextu   이 발송이 **무엇을 위한 것인가** — 소유자가 정한 불변 값.

    work:  canonical 샷 tag · `tag::child` · `groupbg::…` 같은 **구조 키**
    visit: 방문 신원. 모르면 **빈 문자열**(미상).
    r   r   r   r   N)r-   r.   r/   r0   r   r1   r   r3   r$   r#   rY   rY      s     D#NE3Or$   rY   c                 ,    t        t        dt              S )Nsend_ctx)getattr_local
_EMPTY_CTXr3   r$   r#   get_current_send_contextr_      s    6:z22r$   c                0    t               }| t        _        |S r   )r_   r]   r[   )ctxprevs     r#   install_send_contextrc      s    #%DFOKr$   c                  0    e Zd ZdZdddddZd	dZd
dZy)send_contextuZ   소유자가 맥락을 **씌운다**. 자식·공유는 `work` 만 덮었다 복원한다.r   Nr   r   c               P    t        |xs d      | _        || _        t        | _        y )Nr   )r   _work_visitr^   _prev)r"   r   r   s      r#   __init__zsend_context.__init__   s    _
",
r$   c                    t               | _        | j                  | j                  j                  nt	        | j                  xs d      }t        t        | j                  |             t               S )Nr   rf   )r_   rj   ri   r   r   rc   rY   rh   )r"   r   s     r#   	__enter__zsend_context.__enter__   sU    -/
 %)KK$7

  SKK2>[djjFG'))r$   c                .    t        | j                         yNF)rc   rj   r"   exc_typeexctbs       r#   __exit__zsend_context.__exit__   s    TZZ(r$   )r   r   r   zOptional[str]r+   r,   r+   rY   r+   boolr-   r.   r/   r0   rk   rm   rt   r3   r$   r#   re   re      s    d&( -
*r$   re   c                 $    t        t        dd       S )Nledger)r\   r]   r3   r$   r#   get_current_ledgerr{      s    68T**r$   c                0    t               }| t        _        |S )uS   이 스레드의 장부를 갈아 끼우고 **옛 것을 돌려준다**(복원용).)r{   r]   rz   )rz   rb   s     r#   install_ledgerr}      s    DFMKr$   c                D     t               t               d fd}|S )u   지금 스레드의 장부를 **다른 스레드에서도** 보이게 감싼다.

    ★롤 생성은 팬아웃이다 — 안 실으면 그 안의 발송이 통째로 안 잡힌다
     (`bind_current_budget` 과 같은 이유, 같은 관례).
    c                     t              }t              }	  | i |t        |       t        |       S # t        |       t        |       w xY wr   )r}   rc   )argskwargsrb   prev_ctxcapturedcaptured_ctxfns       r#   _wrappedz%bind_current_ledger.<locals>._wrapped   sM    h''5	!t&v& *4  !*4 s	   7 A)r   r   r   r   r+   r   )r{   r_   )r   r   r   r   s   ` @@r#   bind_current_ledgerr      s#     "#H ,-L! Or$   c                  *    e Zd ZdZdddZddZd	dZy)
ledger_scopeu  설치 → **성공·예외 어느 쪽이든 복원**, 그리고 중단까지의 수를 보고.

    ★2026-09-20 Codex BLOCK. 종전에는 정상 흐름에서만 복원·보고했다.
     걷기 도중 상한 예외가 나가면 ①앞 샷들의 발송 보고가 **통째로
     사라지고** ②끝난 걷기의 장부가 **스레드에 남아** 다음 작업이 거기
     들어간다.
    ★보고 중 오류가 **원래 중단 예외를 덮지 않는다** — 보고는 본 작업의
     실패를 가릴 자격이 없다.
    Nc                .    || _         || _        d | _        y r   )_ledger_reportrj   )r"   rz   reports      r#   rk   zledger_scope.__init__   s    +/
r$   c                N    t        | j                        | _        | j                  S r   )r}   r   rj   r!   s    r#   rm   zledger_scope.__enter__   s    #DLL1
||r$   c                    t        | j                         | j                  3| j                  j                  r	 | j                  | j                         yy# t
        $ r Y yw xY wro   )r}   rj   r   r   r7   	Exceptionrp   s       r#   rt   zledger_scope.__exit__   sU    tzz"<<#(=(=T\\*   s   A 	A"!A"r   )rz   'SendLedger'r   r   r+   r,   )r+   r   rv   rx   r3   r$   r#   r   r      s    0
r$   r   r   r:   c           	         t               }|yt               }|j                  | ||||xs |j                  |xs |j                        S )u   **보내기 직전에** 한 번 적는다. 장부가 없으면 아무 일도 안 한다.

    ★반환값이 `None` 일 수 있다 — 호출부는 그대로 진행한다(관측이 본
     작업을 막지 않는다).
    N)r   r   r   r   r   r   )r{   r_   rA   r   r   )r   r   r   r   r   r   ledra   s           r#   record_sendr   
  sT     
C
{ #
$C::4[!(8!.SYY  0 0r$   ru   )ra   rY   r+   rY   )r+   Optional[SendLedger])rz   r   r+   r   )r   r   r+   r   )r   r   r   r   r   r   r   r   r   r   r   r   r+   zOptional[SendAttempt])"r0   
__future__r   rU   r<   dataclassesr   r   typingr   r   r   r	   	GRAIN_RAW
GRAIN_SLOT	GRAIN_SDKr2   r    r'   r*   r   r5   rY   r^   r_   rc   re   localr]   r{   r}   r   r   r   r3   r$   r#   <module>r      s'  &N #   ( , , 	
	 
 ( ( (@ -! -! -!r $   ]
3 0 
	+. @ "00"0,/000,/0 0r$   