
    `gj2                    r   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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ddZddZ	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZ ddZ!ddZ"d d!dZ#d d"dZ$	 d#dd	 	 	 	 	 d$dZ%d#d%dZ&d#d&dZ'd'd(dZ(y))u  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)AnyDict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        J/root/.hermes/venv/lib/python3.12/site-packages/gateway/delivery_ledger.py_db_pathr   I   s    z))r   c                     t               } | j                  j                  dd       t        j                  | d      }|j                  d       |j                  d       |S )NT)parentsexist_ok
   )timeoutzPRAGMA journal_mode=WALa  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
        ))r   parentmkdirsqlite3connectexecute)pathconns     r   _connectr   M   sU    :DKKdT2??4,DLL*+LL	" Kr   c                 r    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.statusr    	Exception)pidr    s     r   _owner_stampr&   f   s?    
))+C9*3/// Dys   & 66c                d   | sy	 t        |       } 	 ddlm}  ||       }|	 t        j                  | d       y|y	 t        |      t        |      k(  S # t        t        f$ r Y yw xY w# t
        $ r d}Y Ww xY w# t        $ r Y yt        $ r Y yt        $ r Y yw xY w# t        t        f$ r Y yw xY w)zFTrue when the recorded owning process still exists (pid + start time).Fr   r   NT)int	TypeError
ValueErrorr#   r    r$   r!   killProcessLookupErrorPermissionErrorOSError)r%   
started_atr    current_starts       r   _owner_aliver1   p   s    #h9.s3 	GGCO =!S_44/ z"    " 	 	 		 z" sP   A A) A: B A&%A&)A76A7:	BBBBB/.B/c                    |  d| d| }t        j                  |j                  dd            j                         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)session_keymessage_refcontentpayloads       r   compute_obligation_idr>      sD    
 Q{m1WI6G>>'..)<=GGI#2NNr   c                F   t        j                          }t               \  }}t        5  t               5 }	|	j	                  d| ||t        |      |rt        |      nd|||||f
       ddd       ddd       t                y# 1 sw Y   xY w# 1 sw Y   t                yxY w)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)timer&   _DB_LOCKr   r   str_prune)
obligation_idr:   platformchat_id	thread_idr<   nowr%   startedr   s
             r   record_obligationrJ      s     ))+C>LC	 

8: 

F
 K3w<(S^dGS#'		


 

 H

 

 

 Hs"   B3B&BB
	BB c                    t        | d       y )N
attempting_update_staterD   s    r   mark_attemptingrP      s    -.r   c                    t        | d       y )N	deliveredrM   rO   s    r   mark_deliveredrS      s    --r   c                     t        | d|       y )Nfailed)errorrM   )rD   rV   s     r   mark_failedrW      s    -7r   c           
         t         5  t               5 }|j                  d|t        j                         |r|d d nd | f       d d d        d d d        y # 1 sw Y   xY w# 1 sw Y   y xY w)NzwUPDATE delivery_obligations
               SET state=?, updated_at=?, last_error=?
               WHERE obligation_id=?r   )rA   r   r   r@   )rD   staterV   r   s       r   rN   rN      sd    	 
8: 
( DIIKt4O		

 
 
 
 
 
s"   A0AAA	AA()deliverable_platformsc               6   | | nt        j                          } t               \  }}g }t        5  t               5 }|j	                  d      j                         }|D ]  \  }}}	}
}}}}}}}t        ||      r|t        k\  s| |z
  t        kD  r|j	                  d| |f       F||	|vrM|j	                  d||| |||f      }|j                  sr|j                  |||	|
|||dk7  |dz   d        	 ddd       ddd       |S # 1 sw Y   xY w# 1 sw Y   |S xY w)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   )rD   r:   rE   rF   rG   r<   needs_markerattempts)r@   r&   rA   r   r   fetchallr1   MAX_ATTEMPTSSTALE_AFTER_SECONDSrowcountappend)rH   rZ   r%   rI   claimedr   rowsoidr:   rE   rF   rG   r<   rY   r_   
created_at	owner_pidowner_started_atcursors                      r   sweep_recoverablerl      sk   * #diikC>LC$&G	 -8: -||E
 (* 	 DH%	 @S+x)Wez9.>I'78<'C*,<@S+ST#J
 %1$99 \\P gsCI>F %(#. (&!*& %*Y$6 (1  5%	- -\ N]- - -\ Ns)   DBD"D0DD	DDc                   | | nt        j                          } | t        z
  }	 t               5 }|j                  d|f       |j                  d      j	                         d   }t        d|t        z
        }|r|j                  d|f       d d d        y # 1 sw Y   y xY w# t        $ r t        j                  dd       Y y 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)
r@   _RETENTION_SECONDSr   r   fetchonemax	_MAX_ROWSr$   loggerdebug)rH   cutoffr   totalexcesss        r   rC   rC     s    #diikC%%FDZ 	4LLS	
 LL;hjE EI-.F% I
	 	 	,  D3dCDs/   
B  ABB  BB  B    CCc                   	 | ddl m}  |       } | j                  d      xs i }|j                  dd      }t        |t              r |j                         j                         dvS t        |      S # t        $ r Y yw xY w)z>Read the ``gateway.delivery_ledger`` config gate (default on).r   )load_configgatewaydelivery_ledgerT>   0noofffalse)	hermes_cli.configry   get
isinstancerB   striplowerboolr$   )configry   gwvalues       r   ledger_enabledr   2  s{    >5 ]FZZ	"(b($/eS!;;=&&(0KKKE{ s   A%A3 (
A3 3	A?>A?c                F   t         5  t               5 }|j                  d| f      j                         }ddd       ddd       t	        j
                  D cg c]!  }|d   |d   |d   |d   |d   |d   |d	   d
# c}d      S # 1 sw Y   SxY w# 1 sw Y   WxY wc c}w )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 ?Nr   r]      r
            )idsessionrY   r_   rh   
updated_at
last_error)indent)rA   r   r   r`   jsondumps)limitr   rf   rs       r   
debug_rowsr   B  s    	 8: ||3 H
 (* 	  :: 	

  dqtad!d!A$ad	
 	 	   	
s'   B"BB&BB	BB)returnzsqlite3.Connection)r   ztuple[int, Optional[int]])r%   r   r/   r   r   r   )r:   rB   r;   rB   r<   rB   r   rB   )rD   rB   r:   rB   rE   rB   rF   rB   rG   zOptional[str]r<   rB   r   None)rD   rB   r   r   ) )rD   rB   rV   rB   r   r   )rD   rB   rY   rB   rV   rB   r   r   )N)rH   Optional[float]rZ   zOptional[set]r   zList[Dict[str, Any]])rH   r   r   r   )r   zOptional[Dict[str, Any]]r   r   )   )r   r(   r   rB   ))__doc__
__future__r   r6   r   loggingr!   r   	threadingr@   typingr   r   r   r   hermes_constantsr	   	getLogger__name__rs   LockrA   ra   rb   ro   rr   RECOVERED_MARKERr   r   r&   r1   r>   rJ   rP   rS   rW   rN   rl   rC   r   r   r   r   r   <module>r      s.  %N #    	    , , ,			8	$9>>
 " % 	
& *2DO  	
    
4/.8
  F ,0F	F )F 	FRD< r   