§
    øžyj€1  ã                  óü   — 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/ragecks/.hermes/hermes-agent/gateway/lifecycle_ledger.pyÚ_process_hermes_homer   =   sW   € å
Œ*�.Š.˜¨Ñ
+Ô
+×
1Ò
1Ñ
3Ô
3€CØ
ð Ý�C‰yŒyÐØ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'   € àÐ#ˆ4ˆ4Õ)=Ñ)?Ô)?€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 )zÁCheap 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Ñ,Ô,ð Ý(+¨D¯JªJ©L¬L¸¬OÑ(<Ô(<�F˜9Ñ%Ø�Eðð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	øøõ
 •Z¥Ð,ð ð ð ØˆðøøøðØ"$ˆØFÐFÐFˆÝ�/¨GÐ4Ñ4Ô4ð 	¸Øð ð �Ø—j’j  aÑ(Ô(¨Ô+�Ø˜&�=�=Ý#& t§z¢z¡|¤|°A¤Ñ#7Ô#7�G˜C‘LÝ˜7‘|”|¥s¨6¡{¤{Ò2Ð2Ø˜øð	ð 	ð 	ñ 	ô 	ð 	ð 	ð 	ð 	ð 	ð 	øøøð 	ð 	ð 	ð 	ð ˜Ð Ð Ø&-¨jÔ&9ˆF�?Ñ#Ø˜WÐ$Ð$Ø*1°.Ô*AˆFÐ&Ñ'Ø˜'Ð!Ð! j°GÐ&;Ð&;Ø&-¨kÔ&:¸WÀZÔ=PÑ&PˆF�?Ñ#øøÝ•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Ð ð ð ð Øˆtˆtðøøøå˜d¥DÑ)Ô)Ð3ˆ4ˆ4¨tÐ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Ý�ŠÐ9ÀDˆÑ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)z�Append 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%  € ð Ð#ˆ4ˆ4Õ)=Ñ)?Ô)?€DØˆ4Œ=Õ-Ð.€DðLØŒ×Ò $°ÐÑ6Ô6Ð6Ø�YŠY�s WˆYÑ-Ô-ð 	=°Ø�HŠH•T”Z µÐ4Ñ4Ô4°tÑ;Ñ<Ô<Ð<ð	=ð 	=ð 	=ñ 	=ô 	=ð 	=ð 	=ð 	=ð 	=ð 	=ð 	=ð 	=øøøð 	=ð 	=ð 	=ð 	=ð 	=ð 	=øåð 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Ð"ð ð ð Øˆuˆuðøøøà�!‚|€|Øˆuð	ð 	/Ð.Ð.Ð.Ð.Ð.àˆ{˜7Ñ#Ô#ð 	Ø�5ð	øåð ð ð ØˆuˆuðøøøàÐØˆtðØ9Ð9Ð9Ð9Ð9Ð9à'Ð'¨Ñ0Ô0ˆØˆ>Ø�4Ý•5˜‘=”=¥5¨Ñ#4Ô#4Ñ4Ñ5Ô5¸Ò<Ð<øÝð ð ð Øˆtˆtðøøø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Ç  € õ Õ5°dÑ;Ô;Ñ<Ô<€HØð �x—|’| GÑ,Ô,°	Ò9Ð9ØˆtÝ! (§,¢,¨uÑ"5Ô"5°x·|²|ÀLÑ7QÔ7QÑRÔRð Øˆtð —\’\ %Ñ(Ô(Ø$ŸLšL¨Ñ6Ô6Ø$ŸLšL¨Ñ6Ô6ð ð  €HðØEÐEÐEÐEÐEÐEåÐ/Ð/°Ñ5Ô5Ñ6Ô6ˆˆøÝð ð ð Øˆˆˆðøøøà	ð 1Ø(*¯ª¨|Ñ(<Ô(<ˆÐ$Ñ%Ø�fŠf�U‰mŒmˆÝ�c�4Ñ Ô ð 	1Ø-0ˆHÐ)Ñ*Ø—G’G˜OÑ,Ô,ˆEØ—G’GÐ/Ñ0Ô0ˆEÝ˜%¥Ñ%Ô%ð 1ØÕ.Ò.Ð.å˜u¥cÑ*Ô*ð /ð  š	˜	Ø ™Õ(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Ñ,Ô,ˆØÐå”l¥8¤<Ñ0Ô0×:Ò:Ñ<Ô<Ø6Ý”y‘{”{ðð ð ð	ˆFõ ˜f dÑ+Ô+Ð+Ý�NŠNðDð —’˜[Ñ)Ô)Ø—’Ð/Ñ0Ô0Ø—’Ð0Ñ1Ô1Ø—’Ð1Ñ2Ô2Ø—’˜_¨eÑ4Ô4ñ	ô 	ð 	øøõ ð Eð Eð EÝ�ŠÐ4¸tˆÑDÔDÐDÐDÐDðEøøøðJÝà"Ý”y‘{”{Ý"œi™kœkÝ&œl­8¬<Ñ8Ô8×BÒBÑDÔDð	ð ð ñ	
ô 	
ð 	
ð 	
øõ ð Jð Jð JÝ�ŠÐ9ÀDˆÑ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¸$Ñ?Ô?Ñ@Ô@ˆØÐ H§L¢L°Ñ$7Ô$7½2¼9¹;¼;Ò$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   