
    Tdjd7                    v   d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	ddl
mZ ddlmZmZmZmZmZ ddlmZ  ej        e          Z ej                    ZdZdZd	Zd
ZdZd Zd=dZd>dZ ed?d            Z!d@dZ"dAdZ#dBd Z$dCd&Z%dDd'Z&dDd(Z'dEdFd+Z(dEdGd-Z)	 dHdd.dId4Z*dHdJd5Z+dHdKd8Z,dLdMd<Z-dS )Nu  Durable delivery-obligation ledger for gateway final responses.

A final agent response that was generated but not yet confirmed-delivered
to the messaging platform is the one artifact the gateway can lose without
a trace: the turn already burned its tokens, the text exists only in a
Python local, and a crash / planned restart between finalize and platform
ACK drops it silently (#58818, #41696, #63695).

This module records a small durable row per outbound final response in the
shared ``state.db`` (same file and conventions as
``tools.async_delegation`` — WAL, owner pid + process-start-time liveness,
bounded retention). The gateway writes three checkpoints around the send:

    record_obligation()   state='pending'     before any send attempt
    mark_attempting()     state='attempting'  immediately before the await
    mark_delivered() /    state='delivered'   only on SendResult.success
    mark_failed()         state='failed'      on a definitive rejection

On startup, ``sweep_recoverable()`` claims rows whose owning process is
dead and hands them to the gateway for redelivery. Crash semantics are
explicit about ambiguity (the contract review of the earlier
delivery-outbox attempt, #61790, closed it for silently resending
ambiguous sends):

- ``pending``     — the send never started: redeliver plainly, no dup risk.
- ``attempting``  — crashed mid-await: the platform MAY already have the
  message. Redelivered WITH a visible recovered-reply marker so the
  contract is honest at-least-once, never a silent duplicate.
- ``failed``      — definitively rejected once; the restart is a natural
  retry boundary. Also carries the marker.
- ``delivered``   — nothing to do; retention prunes.

Poison rows cannot spin: attempts are capped, stale rows expire, and both
transition to ``abandoned`` (kept briefly for inspection, then pruned).

Everything here is best-effort by design: ledger failures must never block
or delay an actual send. Callers wrap every call in try/except.
    )annotationsN)contextmanager)AnyDictIteratorListOptionalget_hermes_home   iQ i:	   u_   ♻️ Recovered reply — the gateway restarted during delivery, so this may be a duplicate:

c                 $    t                      dz  S )Nzstate.dbr
        =/home/thesage/.hermes/hermes-agent/gateway/delivery_ledger.py_db_pathr   J   s    z))r   returnsqlite3.Connectionc                     t                      } | j                            dd           t          j        | d          }	 t          |           n## t          $ r |                                  w xY w|S )NT)parentsexist_ok
   )timeout)r   parentmkdirsqlite3connect_initialize_schema	Exceptionclose)pathconns     r   _connectr#   N   s    ::DKdT222?4,,,D4        	

	
 Ks   A  A2r"   Nonec                V    ddl m}  || d           |                     d           d S )Nr   )apply_wal_with_fallbackzstate.db (delivery_ledger))db_labela  CREATE TABLE IF NOT EXISTS delivery_obligations (
            obligation_id TEXT PRIMARY KEY,
            session_key TEXT NOT NULL,
            platform TEXT NOT NULL,
            chat_id TEXT NOT NULL,
            thread_id TEXT,
            content TEXT NOT NULL,
            state TEXT NOT NULL,
            attempts INTEGER NOT NULL DEFAULT 0,
            created_at REAL NOT NULL,
            updated_at REAL NOT NULL,
            owner_pid INTEGER,
            owner_started_at INTEGER,
            last_error TEXT
        ))hermes_stater&   execute)r"   r&   s     r   r   r   \   sP    444444D+GHHHHLL	    r   Iterator[sqlite3.Connection]c               #     K   t                      } 	 | 5  | V  ddd           n# 1 swxY w Y   |                                  dS # |                                  w xY w)ud  Open a connection, commit/rollback on exit, and ALWAYS close it.

    ``sqlite3.Connection.__enter__``/``__exit__`` only commit or roll back the
    transaction; they do not close the connection. Using ``with _connect()``
    alone therefore leaks a connection — and its WAL/SHM file descriptors — on
    every call, deferring the close to the garbage collector. On a long-running
    gateway that exhausts ``RLIMIT_NOFILE`` (the cron-ledger sibling of this
    bug was #69567 / PR #69594). ``record_obligation`` runs on every outbound
    final response, so this ledger is the highest-frequency leaker.
    N)r#   r    )r"   s    r   _transactionr,   s   s       ::D 	 	JJJ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	



s&   A %A )A )A Atuple[int, Optional[int]]c                 x    t          j                    } 	 ddlm} |  ||           fS # t          $ r | d fcY S w xY w)Nr   get_process_start_time)osgetpidgateway.statusr0   r   )pidr0   s     r   _owner_stampr5      sf    
)++C999999**3////   Dys   ( 99r4   r   
started_atboolc                   | sdS 	 t          |           } n# t          t          f$ r Y dS w xY w	 ddlm}  ||           }n# t
          $ r d}Y nw xY w|B	 t          j        | d           n)# t          $ r Y dS t          $ r Y dS t          $ r Y dS w xY wdS |dS 	 t          |          t          |          k    S # t          t          f$ r Y dS w xY w)zFTrue when the recorded owning process still exists (pid + start time).Fr   r/   NT)int	TypeError
ValueErrorr3   r0   r   r1   killProcessLookupErrorPermissionErrorOSError)r4   r6   r0   current_starts       r   _owner_aliverA      sZ    u#hhz"   uu999999..s33   	GCOOOO! 	 	 	55 	 	 	44 	 	 	55	tt=!!S__44z"   ttsM    ++A AAA, ,
B9	B	BBB< <CCsession_keystrmessage_refcontentc                    |  d| d| }t          j        |                    dd                                                    dd         S )a  Stable id: same turn + same content re-records idempotently, while
    distinct threads/topics on the same chat can never collide (the
    session_key carries platform, chat and thread; ``message_ref`` is the
    triggering inbound message id, distinguishing turns in one session).|zutf-8replaceN   )hashlibsha256encode	hexdigest)rB   rD   rE   payloads       r   compute_obligation_idrO      sR    
 66{66W66G>'..)<<==GGII#2#NNr   obligation_idplatformchat_id	thread_idOptional[str]c                p   t          j                     }t                      \  }}t          5  t                      5 }	|	                    d| ||t          |          |rt          |          nd|||||f
           ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   t                       dS )zBRecord a final response as owed to the platform (state='pending').a'  INSERT OR REPLACE INTO delivery_obligations
               (obligation_id, session_key, platform, chat_id, thread_id,
                content, state, attempts, created_at, updated_at,
                owner_pid, owner_started_at)
               VALUES (?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?, ?, ?)N)timer5   _DB_LOCKr,   r)   rC   _prune)
rP   rB   rQ   rR   rS   rE   nowr4   startedr"   s
             r   record_obligationr[      s0    )++C>>LC	 

 

<>> 

TF
 K3w<<(2S^^^dGS#'		
 		
 		


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 HHHHHs4   B?B:BB
	
BB
	BB!$B!c                &    t          | d           d S )N
attempting_update_staterP   s    r   mark_attemptingra      s    -.....r   c                &    t          | d           d S )N	deliveredr^   r`   s    r   mark_deliveredrd      s    ------r    errorc                *    t          | d|           d S )Nfailed)rf   r^   )rP   rf   s     r   mark_failedri      s    -777777r   statec           
         t           5  t                      5 }|                    d|t          j                    |r
|d d         nd | f           d d d            n# 1 swxY w Y   d d d            d S # 1 swxY w Y   d S )NzwUPDATE delivery_obligations
               SET state=?, updated_at=?, last_error=?
               WHERE obligation_id=?r   )rW   r,   r)   rV   )rP   rj   rf   r"   s       r   r_   r_      s   	 
 
<>> 
T( DIKK!?tt4O		
 	
 	

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s4   A38AA3A	A3"A	#A33A7:A7)deliverable_platformsrY   Optional[float]rl   Optional[set]List[Dict[str, Any]]c               z   | | nt          j                     } t                      \  }}g }t          5  t                      5 }|                    d                                          }|D ]\  }}}	}
}}}}}}}t          ||          r|t          k    s| |z
  t          k    r|                    d| |f           Q||	|vrX|                    d||| |||f          }|j	        r%|
                    |||	|
|||dk    |dz   d           	 ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   |S )ae  Claim undelivered rows owned by dead processes; return them for
    redelivery.

    Claiming atomically re-stamps the owner to THIS process and increments
    ``attempts``, so a second gateway racing the same sweep cannot
    double-claim (the UPDATE is guarded on the previous owner stamp).
    Rows over the attempts cap or older than the stale cutoff transition to
    'abandoned' instead of being returned.

    ``deliverable_platforms`` (platform value strings) restricts claiming to
    platforms the caller can actually send on this boot.  ``attempts`` is the
    redelivery budget, so it must only be spent on a real send: a platform
    that failed to connect would otherwise burn one attempt per boot and hit
    the cap having never been sent once.  Rows for absent platforms are left
    untouched for a later boot; the stale cutoff still bounds them.
    Na  SELECT obligation_id, session_key, platform, chat_id, thread_id,
                      content, state, attempts, created_at,
                      owner_pid, owner_started_at
               FROM delivery_obligations
               WHERE state IN ('pending', 'attempting', 'failed')zlUPDATE delivery_obligations
                       SET state='abandoned', updated_at=? WHERE obligation_id=?zUPDATE delivery_obligations
                   SET owner_pid=?, owner_started_at=?, attempts=attempts+1,
                       updated_at=?
                   WHERE obligation_id=? AND (owner_pid IS ? OR owner_pid=?)pending   )rP   rB   rQ   rR   rS   rE   needs_markerattempts)rV   r5   rW   r,   r)   fetchallrA   MAX_ATTEMPTSSTALE_AFTER_SECONDSrowcountappend)rY   rl   r4   rZ   claimedr"   rowsoidrB   rQ   rR   rS   rE   rj   rt   
created_at	owner_pidowner_started_atcursors                      r   sweep_recoverabler      s-   * ##dikkC>>LC$&G	 - -<>> -T||E
 
 (** 	 DH%	 %	 @S+x)Wez9.>I'788 <''C*,<@S+S+ST#J  
 %1$999 \\P gsCI> F  %(#. (&!*& %*Y$6 (1      5%	- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\ Ns6   D0CDD0D	D0 D	!D00D47D4c                   | | nt          j                     } | t          z
  }	 t                      5 }|                    d|f           |                    d                                          d         }t          d|t          z
            }|r|                    d|f           d d d            d S # 1 swxY w Y   d S # t          $ r  t          	                    dd           Y d S w xY w)NzpDELETE FROM delivery_obligations
                   WHERE state IN ('delivered', 'abandoned') AND updated_at < ?z)SELECT COUNT(*) FROM delivery_obligationsr   a  DELETE FROM delivery_obligations WHERE obligation_id IN (
                         SELECT obligation_id FROM delivery_obligations
                         ORDER BY CASE state
                                    WHEN 'delivered' THEN 0
                                    WHEN 'abandoned' THEN 1
                                    ELSE 2
                                  END, updated_at ASC
                         LIMIT ?)zdelivery ledger prune failedT)exc_info)
rV   _RETENTION_SECONDSr,   r)   fetchonemax	_MAX_ROWSr   loggerdebug)rY   cutoffr"   totalexcesss        r   rX   rX   5  s[   ##dikkC%%FD^^ 	tLLS	  
 LL; hjjE EI-..F % I
 
 
	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	,  D D D3dCCCCCCDs5   C A6B4'C 4B88C ;B8<C &C+*C+configOptional[Dict[str, Any]]c                <   	 | ddl m}  |            } |                     d          pi }|                    dd          }t          |t                    r(|                                                                dvS t          |          S # t          $ r Y dS w xY w)z>Read the ``gateway.delivery_ledger`` config gate (default on).Nr   )load_configgatewaydelivery_ledgerT>   0noofffalse)	hermes_cli.configr   get
isinstancerC   striplowerr7   r   )r   r   gwvalues       r   ledger_enabledr   S  s    >555555 []]FZZ	""(b($//eS!! 	L;;==&&((0KKKE{{   tts   A;B >B 
BB   limitr9   c                   t           5  t                      5 }|                    d| f                                          }ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   t	          j        d |D             d          S )z>Human-readable dump for ad-hoc inspection (sqlite3-free path).zSELECT obligation_id, session_key, state, attempts,
                      created_at, updated_at, last_error
               FROM delivery_obligations
               ORDER BY updated_at DESC LIMIT ?Nc           
     v    g | ]6}|d          |d         |d         |d         |d         |d         |d         d7S )r   rr      r            )idsessionrj   rt   r}   
updated_at
last_errorr   ).0rs     r   
<listcomp>zdebug_rows.<locals>.<listcomp>n  s]     	
 	
 	

  dqtad!d!A$ad 	
 	
 	
r   r   )indent)rW   r,   r)   ru   jsondumps)r   r"   r{   s      r   
debug_rowsr   c  s#   	  <>> T||3 H
 
 (** 	                              :	
 	

 	
 	
 	
 	 	 	 	s4   A$*AA$A	A$A	A$$A(+A()r   r   )r"   r   r   r$   )r   r*   )r   r-   )r4   r   r6   r   r   r7   )rB   rC   rD   rC   rE   rC   r   rC   )rP   rC   rB   rC   rQ   rC   rR   rC   rS   rT   rE   rC   r   r$   )rP   rC   r   r$   )re   )rP   rC   rf   rC   r   r$   )rP   rC   rj   rC   rf   rC   r   r$   )N)rY   rm   rl   rn   r   ro   )rY   rm   r   r$   )r   r   r   r7   )r   )r   r9   r   rC   ).__doc__
__future__r   rJ   r   loggingr1   r   	threadingrV   
contextlibr   typingr   r   r   r   r	   hermes_constantsr   	getLogger__name__r   LockrW   rv   rw   r   r   RECOVERED_MARKERr   r#   r   r,   r5   rA   rO   r[   ra   rd   ri   r_   r   rX   r   r   r   r   r   <module>r      s  % %N # " " " " "    				       % % % % % % 6 6 6 6 6 6 6 6 6 6 6 6 6 6 , , , , , ,		8	$	$9>
 " % 	
& * * *      .    &      DO O O O   4/ / / /. . . .8 8 8 8 8
 
 
 
 
  F ,0F F F F F FRD D D D D<           r   