
    Pmj1                        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mZmZ ddl	m
Z
 ddlmZmZmZ  ej        e          ZdZdZd	Zd
Zd*dZd+d,dZd-dZd.dZd/dZd0dZd1dZd+d2d Zd+d2d!Z	 	 	 d3d4d'Zd5d)Z dS )6u  Gateway lifecycle ledger — durable termination-reason evidence (NS-608).

The gateway already has *graceful* shutdown forensics
(:mod:`gateway.shutdown_forensics` — who sent the SIGTERM) and an exit-path
diagnostic log (``gateway-exit-diag.log`` — every way ``asyncio.run`` can
return).  What it does NOT have is any record of an **unclean death**: a
SIGKILL, a kernel OOM kill, or the whole VM dying takes the process out
before any handler runs, so the next boot has no idea the previous life
ended violently — support tickets like NS-608 then require manually
cross-correlating four log files and two external APIs to answer "what
killed the gateway?".

This module closes that gap with a tiny state machine persisted to
``<HERMES_HOME>/state/gateway.lifecycle.json``:

* On startup, :func:`record_startup` reads the sentinel left by the
  previous life.  ``phase == "running"`` means that life never reached any
  exit path → it died uncleanly.  The finding — including the last
  heartbeat's memory sample, which is the closest thing to a pre-death
  telemetry snapshot — is appended to ``gateway-exit-diag.log`` as a
  ``gateway.previous_unclean_exit`` record and logged at WARNING.  The
  sentinel is then rewritten as ``phase=running`` for the new life.
* On every clean exit path, :func:`mark_exited` rewrites the sentinel as
  ``phase=exited`` with the exit code and a reason string.  Wired into
  ``_exit_after_graceful_shutdown`` (the single funnel for all graceful
  exits, #53107) and the two watchdog ``os._exit`` sites in
  :mod:`gateway.shutdown_watchdog`.

:func:`sample_memory` provides the cheap (<1ms, pure /proc reads) memory
snapshot that :func:`gateway.shutdown_watchdog.write_loop_heartbeat`
embeds in the 30s heartbeat — giving every unclean-death report a
"memory available N seconds before death" data point so OOM crash cycles
are classifiable from the volume alone (no Prometheus retention races).

Everything here is best-effort: a forensics failure must never affect the
gateway lifecycle it is observing.
    )annotationsN)datetimetimezone)Path)AnyDictOptional)statezgateway.lifecycle.json)logszgateway-exit-diag.logi   g?returnr   c                     t           j                            dd                                          } | rt	          |           S ddlm}  |            S )zEHERMES_HOME for process-level identity files (ignore task overrides).HERMES_HOME r   )get_hermes_home)osenvirongetstripr   hermes_constantsr   )valr   s     >/home/thesage/.hermes/hermes-agent/gateway/lifecycle_ledger.py_process_hermes_homer   =   sW    
*..
+
+
1
1
3
3C
 Cyy000000?    homeOptional[Path]c                D    | | nt                      } |j        t           S )z6Return ``<HERMES_HOME>/state/gateway.lifecycle.json``.)r   joinpath_LIFECYCLE_RELATIVE)r   bases     r   get_lifecycle_sentinel_pathr    G   s'    #44)=)?)?D4=-..r   Dict[str, Any]c                    i } 	 t          dd          5 }|D ]C}|                    d          r,t          |                                d                   | d<    nDddd           n# 1 swxY w Y   n# t          t
          t          f$ r Y nw xY w	 i }h d}t          d	d          5 }|D ]n}|                    d
d          d         }||v rLt          |                                d                   ||<   t          |          t          |          k    r noddd           n# 1 swxY w Y   d|v r|d         | d<   d|v r|d         | d<   d|v rd|v r|d         |d         z
  | d<   n# t          t
          t          f$ r Y nw xY w| S )zCheap memory snapshot: own RSS + system availability + swap.

    Pure ``/proc`` reads, Linux-only (returns ``{}`` elsewhere), never
    raises.  Values in KiB to match the kernel's units.
    z/proc/self/statusutf-8encodingzVmRSS:   rss_kibN>   MemTotalSwapFree	SwapTotalMemAvailablez/proc/meminfo:r   r(   mem_total_kibr+   mem_available_kibr*   r)   swap_used_kib)open
startswithintsplitOSError
ValueError
IndexErrorlen)samplefhlinememinfowantedkeys         r   sample_memoryr>   M   s_     F%888 	B  ??8,, (+DJJLLO(<(<F9%E	 	 	 	 	 	 	 	 	 	 	 	 	 	 	
 Z,   "$FFF/G444 	  jja((+&==#&tzz||A#7#7GCL7||s6{{22	 	 	 	 	 	 	 	 	 	 	 	 	 	 	   &-j&9F?#W$$*1.*AF&''!!jG&;&;&-k&:WZ=P&PF?#Z,   Msl   A4 AA(A4 (A,,A4 /A,0A4 4BBE- )A2D'E- 'D++E- .D+/=E- -FFpathOptional[Dict[str, Any]]c                    	 t          j        |                     d                    }n# t          t          f$ r Y d S w xY wt          |t                    r|nd S )Nr#   r$   )jsonloads	read_textr4   r5   
isinstancedict)r?   datas     r   
_read_jsonrH   q   sf    z$..'.::;;Z    ttdD))344t3s   (+ A A payloadNonec                    t          |          }	 ddlm} |j                            dd            ||| d            d S # t
          $ r  t                              dd           Y d S w xY w)Nr   )atomic_json_writeTparentsexist_ok)indentz"Failed to write lifecycle sentinelexc_info)r    utilsrL   parentmkdir	Exceptionloggerdebug)rI   r   r?   rL   s       r   _write_sentinelrY   y   s    &t,,DJ++++++$666$555555 J J J9DIIIIIIJs   0A &A-,A-recordc                   ||nt                      } |j        t           }	 |j                            dd           |                    dd          5 }|                    t          j        | t                    dz              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)zAppend a JSON line to gateway-exit-diag.log (same format as the CLI's
    ``_exit_diag`` records so existing tooling greps both).NTrM   ar#   r$   )default
z$Failed to append unclean-exit recordrQ   )r   r   _EXIT_DIAG_RELATIVErT   rU   r0   writerB   dumpsstrr4   rW   rX   )rZ   r   r   r?   r9   s        r   _append_exit_diagrc      s%    #44)=)?)?D4=-.DL$666YYsWY-- 	=HHTZ444t;<<<	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= L L L;dKKKKKKLs5   3B" 2BB" BB" BB" "&CCpidr   
start_timeboolc                f   	 t          |           }n# t          t          f$ r Y dS w xY w|dk    rdS 	 ddlm}  ||          sdS n# t
          $ r Y dS w xY w|dS 	 ddlm}  ||          }|dS t          t          |          t          |          z
            dk    S # t
          $ r Y dS w xY w)u  True when ``pid`` is a live process matching ``start_time`` (±2s).

    Guards the takeover race: during ``--replace`` the old gateway can still
    be mid-teardown when the new one boots — a live matching owner is a
    planned handover, not an unclean death.
    Fr   )_pid_existsNT)get_process_start_timeg       @)	r2   	TypeErrorr5   gateway.statusrh   rV   ri   absfloat)rd   re   pid_intrh   ri   actuals         r   _pid_alive_with_start_timerp      s$   c((z"   uu!||u	 	/.....{7## 	5	   uut999999''00>45==5#4#4455<<   tts5    ''A 
AAB" 2/B" "
B0/B0c                L   t          t          |                     }|r|                    d          dk    rdS t          |                    d          |                    d                    rdS |                    d          |                    d          |                    d          d}	 dd	lm} t           ||                     }n# t          $ r d}Y nw xY w|r|                    d
          |d<   |                    d          }t          |t                    r}||d<   |                    d          }|                    d          }t          |t                    r9|t          k     s)t          |t                    r|dk    r||z  t          k     rd|d<   |S )u   Inspect the previous life's sentinel; return an evidence dict when it
    died uncleanly, else ``None``.  Read-only — does not rewrite the sentinel.
    phaserunningNrd   re   
started_at)	prior_pidprior_started_atprior_start_timer   )get_loop_heartbeat_path
updated_atlast_heartbeat_atmemlast_heartbeat_memr-   r.   Tsuspected_oom)rH   r    r   rp   gateway.shutdown_watchdogrx   rV   rE   rF   r2   _LOW_MEM_AVAILABLE_KIB_LOW_MEM_AVAILABLE_FRACTION)r   sentinelevidencerx   hbr{   totalavails           r   detect_unclean_exitr      s    5d;;<<H x||G,,	99t!(,,u"5"5x||L7Q7QRR t \\%(($LL66$LL66   HEEEEEE//5566   	 1(*|(<(<$%ffUmmc4   	1-0H)*GGO,,EGG/00E%%% 1...uc** / 		(CCC -1)Os   2C C C c                Z   d}	 t          |           }|t          j        t          j                                                  dt          j                    d|}t          ||            t          
                    d|                    d          |                    d          |                    d          |                    d          |                    d	d
                     n,# t          $ r t                              dd           Y nw xY w	 t          dt          j                    t          j                    t          j        t          j                                                  d|            n,# t          $ r t                              dd           Y nw xY w|S )a  Boot-time entry point: report any unclean previous exit, then claim
    the sentinel for the current life.

    Returns the unclean-exit evidence dict (also persisted to
    ``gateway-exit-diag.log`` and logged at WARNING) or ``None``.  Never
    raises.
    Nzgateway.previous_unclean_exit)tstagrd   u   Previous gateway life (pid=%s, started_at=%s) exited UNCLEANLY (no exit path ran — SIGKILL / OOM / VM death). last_heartbeat_at=%s last_mem=%s suspected_oom=%sru   rv   rz   r|   r}   FzUnclean-exit detection failedTrQ   rs   )rr   rd   re   rt   z"Failed to claim lifecycle sentinel)r   r   nowr   utc	isoformatr   getpidrc   rW   warningr   rV   rX   rY   time)r   r   rZ   s      r   record_startupr      s    *.HE&t,,l8<00::<<6y{{  	F fd+++NND [))/00011122_e44	 	 	  E E E4tDDDDDEJ"y{{"ikk&l8<88BBDD	  	
 	
 	
 	
  J J J9DIIIIIJOs%   C'C, ,&DDA%E? ?&F('F(graceful_shutdown	exit_codeOptional[int]reasonrb   c           	        	 t          t          |                    }|,|                    d          t          j                    k    rdS t          dt          j                    | |t          j        t          j	                  
                                d|           dS # t          $ r  t                              dd           Y dS w xY w)u+  Mark the current life as cleanly exited.  Idempotent, never raises.

    Only rewrites the sentinel when it is provably owned by this process —
    during a ``--replace`` takeover the replacement claims the sentinel
    before the old process finishes teardown, and the old life must not
    clobber the new owner's ``running`` phase on its way out.  A sentinel
    with ``pid=None`` (or a malformed pid) has *unknown* ownership and is
    likewise left alone: we must not overwrite evidence we cannot prove is
    ours with a ``clean exit`` claim.
    Nrd   exited)rr   rd   r   exit_reason	exited_atz(Failed to mark lifecycle sentinel exitedTrQ   )rH   r    r   r   r   rY   r   r   r   r   r   rV   rW   rX   )r   r   r   r   s       r   mark_exitedr     s    P9$??@@HLL$7$729;;$F$FF!y{{&%%\(,77AACC  		
 		
 		
 		
 		
  P P P?$OOOOOOPs   AB# AB# #&CCprofile_homec                    	 t          t          |                     }|sdS |                    d          }|dk    rdS |dk    rdS n# t          $ r Y nw xY wdS )u  Container-boot helper: one-word summary of how the profile's last
    gateway life ended.  ``clean`` / ``unclean`` / ``unknown`` (no sentinel
    or never ran).  Read-only and exception-free — used by
    ``hermes_cli.container_boot`` to annotate ``container-boot.log``.
    unknownrr   r   cleanrs   unclean)rH   r    r   rV   )r   r   rr   s      r   read_prior_exit_labelr   0  s    9,GGHH 	9W%%H7I 9     9s   A A A 
AA)r   r   )N)r   r   r   r   )r   r!   )r?   r   r   r@   )rI   r!   r   r   r   rJ   )rZ   r!   r   r   r   rJ   )rd   r   re   r   r   rf   )r   r   r   r@   )Nr   N)r   r   r   rb   r   r   r   rJ   )r   r   r   rb   )!__doc__
__future__r   rB   loggingr   r   r   r   pathlibr   typingr   r   r	   	getLogger__name__rW   r   r_   r   r   r   r    r>   rH   rY   rc   rp   r   r   r   r    r   r   <module>r      s  $ $L # " " " " "   				  ' ' ' ' ' ' ' '       & & & & & & & & & &		8	$	$9 7 
 # "    / / / / /! ! ! !H4 4 4 4J J J J
L 
L 
L 
L! ! ! !H( ( ( ( (V, , , , ,`  $%P P P P PB     r   