
    Pmj!*                        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m	Z	 ddl
mZmZmZ  ej        e          Zd Zd!dZd"dZddd#dZd$dZ	 d%d&dZd'd ZdS )(u  Flush pending messages and agent transcripts to disk before shutdown to prevent data loss.

When FTS5 index corruption prevents ``INSERT INTO messages``, the gateway
accumulates messages in ``_pending_messages`` (memory-only) and the live
``agent._session_messages`` cannot be flushed via ``_flush_messages_to_session_db``.
On shutdown, ``.clear()`` discards the only surviving copy — permanent user data loss.

This module provides three hooks:

1. ``flush_pending_to_file()`` — called BEFORE ``_pending_messages.clear()``
   during shutdown.  Serialises any non-empty pending slots to a JSON file
   under ``<hermes_home>/pending_messages/``.

2. ``recover_pending_to_db()`` — called AFTER ``runner.start()`` on startup.
   Reads flush files, inserts messages into state.db via ``SessionDB.append_message``
   (so FTS indexing, session metadata, and display_kind are handled correctly),
   then deletes the flush file on success.

3. ``flush_agent_history_to_file()`` — called from ``_finalize_shutdown_agents``
   when ``_flush_messages_to_session_db`` raises.  Dumps the live
   ``agent._session_messages`` to the same atomic JSON recovery directory.

See issue #72680 for the full incident report.
    )annotationsN)Path)AnyDictOptionalc                     ddl m}   |             dz  }|                    ddd           t          j        dk    rt          j        |d           |S )zIReturn the pending-messages flush directory under the active HERMES_HOME.r   )get_hermes_homepending_messagesTi  )parentsexist_okmodeposix)hermes_constantsr	   mkdirosnamechmod)r	   	flush_dirs     </home/thesage/.hermes/hermes-agent/gateway/shutdown_flush.py_get_flush_dirr   '   sd    000000!!$66IOOD4eO<<<	w'
E"""    pathr   returnNonec                    t           j        dk    rdS t          j        | t           j                  }	 t          j        |           t          j        |           dS # t          j        |           w xY w)zDPersist a directory entry on platforms that support directory fsync.r   N)r   r   openO_RDONLYfsyncclose)r   directory_fds     r   _fsync_directoryr!   2   sh    	w'74--L

s   A A3r   payloadDict[str, Any]c                   ddl m} t          j                    j        }| d| dz  } |||dt
                     	 t          |            dS # t          $ r&}t          	                    d|           Y d}~dS d}~ww xY w)	z>Atomically write one private, uniquely named recovery payload.r   )atomic_json_writezpending-z.jsoni  )r   defaultz-Failed to fsync pending-message directory: %sN)
utilsr%   uuiduuid4hexstrr!   OSErrorloggerdebug)r   r"   r%   file_id
final_pathexcs         r   _write_payloadr2   =   s    ''''''jllG66666J	   K##### K K K 	DcJJJJJJJJJKs   A 
A>A99A>shutdown)reasonpendingr4   r+   intc          	        | sdS t                      }t          t          j                              }d}t          |                                           D ]h\  }}|	 t          |          }|t          |||||d           |dz  }6# t          $ r&}t          	                    d||           Y d}~ad}~ww xY w|rt          
                    d|||           |S )a  Serialise non-empty ``_pending_messages`` slots to disk.

    Parameters
    ----------
    pending:
        The adapter or runner ``_pending_messages`` dict.  Values may be
        ``MessageEvent`` objects (adapter) or plain strings (runner).
    reason:
        Logged context (``shutdown``, ``restart``, etc.).

    Returns
    -------
    int
        Number of sessions flushed.
    r   N)session_keyr4   tsdata   z*Failed to flush pending message for %s: %sz/Flushed %d pending message(s) to %s (reason=%s))r   r6   timelistitems_serialise_valuer2   	Exceptionr-   r.   info)	r5   r4   r   r9   flushedr8   value
serialisedr1   s	            r   flush_pending_to_filerE   R   s:   (  q  I	TY[[		BG"7==??33  U=	)%00J!#.$&	    qLGG 	 	 	LL<S       	  
=Y	
 	
 	
 Ns   B1B
B<B77B<rC   r   Optional[dict]c                    t          | d          rqdt          | dd          i}dD ]Y}t          | |d          }|D	 t          j        |           |||<   0# t          t
          f$ r t          |          ||<   Y Uw xY wZ|S t          | t                    rd| iS t          | t                    r?	 t          j        |            | S # t          t
          f$ r dt          |           icY S w xY wdt          |           iS )z<Convert a pending message value to a JSON-serialisable dict.text )
session_idplatform	sender_idsender_namereply_tomedia	raw_eventN)	hasattrgetattrjsondumps	TypeError
ValueErrorr+   
isinstancedict)rC   resultattrvals       r   r?   r?      s?    uf "('%*D*D!E8 	, 	,D%t,,C,JsOOO#&F4LL!:. , , ,#&s88F4LLL,	  % % (	(JuL:& 	( 	( 	(CJJ''''	(CJJs#   A#A<;A<1C "C,+C,c                   t                      }t          |                    d                    }|sdS d}| ddlm}  |            } d}d}|D ]w}	 t          j        |                    d                    }|                    d	          d
k    rF|                    dd          }|                    di           }	|	                    dd          }
|
r|st          
                    d|           |	                    dd          }|st          
                    d||           |                     |d|
|                    dt          t          j                                                   |dz  }|                    d           E# t          $ r'}t          
                    d||           Y d}~qd}~ww xY w|r&	 |                                  n# t          $ r Y nw xY w|rt                              d|           |S )a.  Recover flushed pending messages into state.db via SessionDB.

    Reads all ``*.json`` files from the flush directory, inserts messages
    using ``SessionDB.append_message`` (so FTS indexing, session metadata
    updates, and all required columns are handled correctly), and deletes
    the flush file on success.

    Parameters
    ----------
    session_db:
        An existing ``SessionDB`` instance.  If ``None``, a new one is
        opened on the default ``state.db`` path.

    Returns
    -------
    int
        Number of messages recovered.
    z*.jsonr   FN)	SessionDBTzutf-8)encodingr4   'shutdown-with-unpersisted-agent-historyr8   rI   r:   rH   z^Cannot recover structurally invalid pending message from %s; the flush file has been preservedrJ   zCannot recover pending message for %s: no session_id in flush file and session_key-to-id resolution is not available at this recovery stage. The message text is preserved in %suserr9   )rJ   rolecontent	timestampr;   )
missing_okz-Failed to recover pending message from %s: %sz3Recovered %d pending message(s) from shutdown flush)r   sortedglobhermes_stater]   rS   loads	read_textgetr-   warningappend_messager6   r<   unlinkr@   r   rA   )
session_dbr   flush_filesown_dbr]   	recoveredr   r"   r8   r:   rH   rJ   r1   s                r   recover_pending_to_dbrr      s   *   I1122K q F******Y[[
I 5 54	j!A!ABBG {{8$$(QQQ!++mR88K;;vr**D88FB''D { 8  
  ,33J 
 &     %%%!++dC	,<,<==	 &    NIKK4K(((( 	 	 	NN?c       	  	 	 	 	D	  
A9	
 	
 	
 s?   AFA!F74F,A&F
GG  GG   
G-,G-rJ   Optional[str]historyr=   c                (   |sdS 	 t                      }g }|D ]y}	 |                    t          |t          t          t
          t          t          t          t          d          f          r|nt          |                     j# t          $ r Y vw xY wt          |dd| t          |          |d           t                              dt          |          |            dS # t          $ r'}t                              d| |           Y d}~dS d}~ww xY w)u   Best-effort dump of an agent's in-memory transcript before teardown.

    Used when ``_flush_messages_to_session_db`` raises (e.g. FTS/SQLite
    index corruption, #72680): the live ``agent._session_messages`` could
    not be written to disk, and a plain debug log would lose it permanently
    when the process exits. Serialize to an atomic JSON file outside the
    broken DB so an operator can salvage the conversation after repairing
    state.db.

    Failures are swallowed — shutdown must never block on a best-effort
    backup.
    Nr_   z#72680)r4   issuerJ   countmessagesuo   Preserved %d in-memory message(s) for session %s (possible FTS corruption — recover after repairing state.db)z=Agent-history shutdown preservation failed for session %s: %s)r   appendrW   rX   r=   r+   r6   floatbooltyper@   r2   lenr-   rk   )rJ   rt   r   snapshot_m_es         r   flush_agent_history_to_filer     sq      
"$$	 	 	B$R$c3tTRVZZ)XYY !BBR       C!(X$ 		
 		
 		
 	MMM		
 	
 	
 	
 	
  
 
 
K	
 	
 	
 	
 	
 	
 	
 	
 	

s<   C  A&BC  
BC  BAC   
D*DD)r   r   r   r   )r   r   r"   r#   r   r   )r5   r#   r4   r+   r   r6   )rC   r   r   rF   )N)r   r6   )rJ   rs   rt   r=   r   r   )__doc__
__future__r   rS   loggingr   r<   r(   pathlibr   typingr   r   r   	getLogger__name__r-   r   r!   r2   rE   r?   rr   r    r   r   <module>r      sG   2 # " " " " "   				         & & & & & & & & & &		8	$	$     K K K K0 7 7 7 7 7 7t       < d d d d dN1
 1
 1
 1
 1
 1
r   