
    *j9              
          U d 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Z	ddl
Z	ddlmZ ddlmZ ddlmZ ddlmZmZmZ 	 ddlmZ n# e$ r dZY nw xY wddddd	Zd
Z e            Zeed<    ej                    Zda ej                    ZdZdZ  ej!        d          Z" ej!        d          Z# ej!        dej$                  Z%e dde&de'de&fdZ(de'fdZ)de&de'de*fdZ+d_dZ,d`dede&fdZ-d dd!dede&dz  fd"Z.de&fd#Z/d$ede&dz  fd%Z0de&fd&Z1d'e2de&dz  fd(Z3dad*e4de&dz  fd+Z5de&fd,Z6 e/            Z7e&ed-<    e6            Z8e&ed.<   d/ Z9d0 Z:d1 Z;d2 Z<d3 Z=d4 Z>d5 Z?d6 Z@d7 ZAd8 ZBdd9d:ZCd; ZDde*fd<ZEdbdd=d>ZFde&fd?ZGd@ddAe*de'deHe&         fdBZId@ddAe*de'deJeHe&         eKf         fdCZLdDe*dEeHe*         de&fdFZMdGe&de&fdHZNdGe&deJe&dz  e&f         fdIZOdJeHe&         deHe&         fdKZPdLe&dMeHe&         deHe&         fdNZQdLe&deJeHe&         eHe&         f         fdOZRdEeHe*         deHe&         fdPZSdEeHe*         deHe&         fdQZTdRe&dEeHe*         deJeHe*         e&f         fdSZUdDe*dEeHe*         de&fdTZVdEeHe*         deJe&e&f         fdUZWdcdddVdDe*de&dz  dWeKde*fdXZXdddZe4ddfd[ZYde&de*fd\ZZd] Z[d^ Z\dS )eaL  
Hermes Web UI -- Self-update checker.

Checks if the webui and hermes-agent git repos are behind their latest
release tags. Results are cached server-side (30-min TTL) so git fetch runs
at most twice per hour regardless of client count.

Skips repos that are not git checkouts (e.g. Docker baked images where
.git does not exist).
    N)OrderedDict)Path)urlparse)	REPO_ROOTSTREAMSSTREAMS_LOCK)
_AGENT_DIRT)webuiagent
checked_atinclude_agent   _summary_cacheFi  ,  z*([a-zA-Z][a-zA-Z0-9+.-]*://)([^/@\s'\"]+)@z@\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,})\bz9([?&](?:access_token|token|password|auth|key)=)[^&\s'\"]+limitoutputr   returnc                V   | sdS t                               dt          |                     }t                              d|          }t                              d|          }|                                }t          |          |k    r|d|                                         dz   }|S )a.  Return a user-facing git diagnostic with credentials removed.

    Git can echo remote URLs in failure output.  Keep the actionable error text,
    but strip URL userinfo, common GitHub token shapes, and secret-looking query
    parameter values before any message reaches the update-check API/UI.
     z\1<redacted>@z
<redacted>z\1<redacted>Nu   …)_CREDENTIAL_IN_URL_REsubstr_GITHUB_TOKEN_RE_QUERY_SECRET_REstriplenrstrip)r   r   	sanitizeds      ./Users/bertmahoney/hermes-webui/api/updates.py_sanitize_git_diagnosticr!   -   s      r%))*:CKKHHI $$\9==I $$_i@@I!!I
9~~fuf%,,..6	    c                  l    t           5  t          t                    cddd           S # 1 swxY w Y   dS )ao  Return the current in-memory chat stream count.

    Self-update schedules an in-process re-exec after git pull/reset.  That is
    restart-equivalent for live streams, even when systemd does not see a unit
    restart.  Refuse update/force-update while a stream exists so a browser
    update click cannot recreate the pending-message loss class fixed in #1543.
    N)r   r   r    r"   r    _active_stream_countr%   ?   sw     
  7||                 s   )--targetactive_streamsc                 :    |dk    rdnd}dd|  d| d| d| d	|d
S )N   sr   FzCannot update z while z active chat streamzD is running. Wait for the response to finish, then retry the update.T)okmessager&   restart_blockedr'   r$   )r&   r'   plurals      r    _restart_blocked_responser/   K   sh    "a''SSRFRV R RN R Rv R R R (	 	 	r"   
   c                 (   	 t          j        dg| z   t          |          dd|          }|j                                        }|j                                        }|j        dk    r|dfS |p|p	d|j         dfS # t           j        $ r`}t          |dd          pt          |d	d          pd
                                }|pdd	                    |            d| ddfcY d}~S d}~wt          $ r Y dS t          $ r}d| dfcY d}~S d}~ww xY w)zRun a git command and return (useful output, ok).

    On failure, returns stderr (or stdout as fallback) so callers can
    surface actionable git error messages instead of empty strings.
    gitT)cwdcapture_outputtexttimeoutr   zgit exited with status FstderrNstdoutr   zgit  z timed out after r*   )zgit executable not foundFzgit failed to start: )
subprocessrunr   r8   r   r7   
returncodeTimeoutExpiredgetattrjoinFileNotFoundErrorOSError)argsr3   r6   rr8   r7   excdetails           r    _run_gitrF   Y   sy   4NGdNCw
 
 
 !!!!<14<KK#KQ\#K#KURR$ S S S#x..T'#x2N2NTRT[[]]KKKKKKKURRRRRRR 1 1 1000 4 4 4,s,,e33333334s=   A*A= -A= =DAC'!D'D7	D DDDr)   pathc                 D    t          g d| |          \  }}|rdS |sdndS )zHReturn a best-effort ``-dirty`` suffix without blocking version display.)z
diff-index--quietHEADz--r6   r   z-dirtyrF   )rG   r6   outr+   s       r    _dirty_suffixrN   r   s?    >>>gVVVGC	 r &88B&r"      )r6   dirty_timeoutc                d    t          g d| |          \  }}|r|sdS |t          | |          z   S )z>Return a fast git version string for a checkout, if available.describe--tagsz--alwaysrK   N)rF   rN   )rG   r6   rP   rM   r+   s        r    _describe_git_versionrU   |   sM    9994QQQGC 3 tt];;;;;r"   c                  &   t          t                    } | r| S t          dz  dz  }|                                rW	 ddl}|                    d|                    d                    }|r|                    d          S n# t          $ r Y nw xY wd	S )
u  Detect the running WebUI version from git or a baked-in fallback file.

    Resolution order:
      1. ``git describe --tags --always --dirty`` — works in any git checkout.
         Returns the exact tag on tagged commits (e.g. ``v0.50.124``), a
         post-tag descriptor between releases (e.g. ``v0.50.124-1-ge91325d``),
         or a bare SHA when no tags exist (shallow clones, fresh forks).
      2. ``api/_version.py`` — a fallback written by the Docker / CI release
         workflow when ``.git`` is not present in the image.  Expected to define
         ``__version__ = 'vX.Y.Z'``.
      3. ``'unknown'`` — last resort; displayed as-is in the settings badge.
    apiz_version.pyr   N"__version__\s*=\s*['"]([^'"]+)['"]utf-8encodingr)   unknown)rU   r   existsresearch	read_textgroup	Exception)rM   version_file_rems       r    _detect_webui_versionrf      s      	
*
*C
 

 u$}4L 
		

9&&&88 A  "wwqzz!" 	 	 	D	 9s   AB 
BB	agent_dirc                 @   | dz  dz  }	 |                     d          }n# t          t          f$ r Y dS w xY wt          j        d|          }|rN|                    d                                          r'|                    d                                          S dS )z>Read Hermes Agent's package version from a copied source tree.
hermes_cliz__init__.pyrY   rZ   NrX   r)   )r`   rA   UnicodeDecodeErrorr^   r_   ra   r   )rg   	init_filer5   re   s       r    _read_agent_source_versionrl      s    L(=8I""G"44'(   tt
	;TBBA "QWWQZZ "wwqzz!!!4s   ! 66c                     t           j                            d          p t           j                            d          pd                                } |                     d          r| dt          d                    } n-|                     d          r| dt          d                    } |                     d          S )z<Return the configured/default Hermes Agent gateway base URL.GATEWAY_HEALTH_URLHERMES_GATEWAY_HEALTH_URLzhttp://hermes-agent:8642/health/detailedN/health/)osenvirongetr   endswithr   r   )raws    r    _gateway_health_base_urlrx      s     	
+,, 	&:>>566	&%egg	 
 ||&'' %,S+,,,,-	i	 	  %#S^^O#$::c??r"   payloadc                    t          | t                    sdS dD ]V}|                     |          }t          |t                    r*|                                r|                                c S W|                     d          }t          |t                    rR|                    d          }t          |t                    r(|                                r|                                S dS )zDExtract a version string from a Hermes Agent gateway health payload.N)versionagent_versionhermes_versionr   r{   )
isinstancedictru   r   r   )ry   keyvaluenesteds       r    $_version_from_gateway_health_payloadr      s    gt$$ t= ! !C  eS!! 	!ekkmm 	!;;==   [[!!F&$ !

9%%eS!! 	!ekkmm 	!;;== 4r"         ?r6   c                     t                      }|sdS t          |          }|j        dvs|j        sdS dD ]}	 t          j                            | | |           5 }t          j        |	                                
                    d                    }ddd           n# 1 swxY w Y   n8# t          t          j        j        t          t          j        t           f$ r Y w xY wt#          |          }|r|c S dS )zCBest-effort cross-container gateway API fallback for Agent version.Nhttphttps)rq   rp   rK   rY   )rx   r   schemenetlocurllibrequesturlopenjsonloadsreaddecoderA   errorURLErrorTimeoutErrorJSONDecodeErrorrj   r   )r6   baseparsedrG   respry   r{   s          r    )_detect_agent_version_from_gateway_healthr      sZ   #%%D td^^F}---V]-t/  	''4'II BT*TYY[[%7%7%@%@AAB B B B B B B B B B B B B B B.d>RTfg 	 	 	H	6w?? 	NNN	4s5   %B0:B$B0$B(	(B0+B(	,B002C%$C%c                     t           t          t                     nd} | | dz  }	 |                                r,|                    d                                          }|r|S n# t
          $ r Y nw xY w|                                 r&t          |           }|r|S t          |           }|r|S t                      }|r|S dS )z7Detect the running Hermes Agent version for UI display.NVERSIONrY   rZ   znot detected)	r	   r   r]   r`   r   rb   rU   rl   r   )rg   rc   r5   rM   source_versiongateway_versions         r    _detect_agent_versionr      s    $.$:Z   I 9,	""$$  #--w-??EEGG  K 	 	 	D	
  	& (	22C 
 8	BBN &%%?AAO >s   ?A' '
A43A4WEBUI_VERSIONAGENT_VERSIONc                 F   | s| S |                                  } |                     d          r,|                     ddd                              ddd          } |                     d          } |                     d          r
| dd         } |                     d          S )	uO  Return the browser-facing repository URL for update compare links.

    Git remotes may be HTTPS or SSH and may include a literal ``.git`` suffix.
    Strip only that literal suffix — never use ``str.rstrip('.git')`` because it
    treats the argument as a character set and can truncate ``hermes-webui`` to
    ``hermes-webu``.
    zgit@:rr   r)   zhttps://.gitN)r   
startswithreplacer   rv   )
remote_urls    r    _normalize_remote_urlr     s      !!##JV$$ T''S!44<<VZQRSS
""3''J6"" %_
S!!!r"   c                 h    | r|r|sdS t          |           }|j        dvs|j        sdS |  d| d| S )zEReturn a safe browser compare URL, or None when any piece is missing.Nr   z	/compare/z...)r   r   r   )repo_urlcurrent_sha
latest_shar   s       r    _build_compare_urlr   (  s`       thF}---V]-t=======r"   c                 L    d| vrd| fS |                      dd          \  }}||fS )zrSplit 'origin/branch-name' into ('origin', 'branch-name').

    Returns (None, ref) if ref contains no slash.
    rr   Nr)   )split)refremotebranchs      r    _split_remote_refr   2  s8    
 #~~SyYYsA&&NFF6>r"   c                     t          ddg|           \  }}|r|r|                    d          d         S dD ]!}t          ddd| g|           \  }}|r|c S "d	S )
z2Detect the remote default branch (master or main).zsymbolic-refzrefs/remotes/origin/HEADrr   )mastermain	rev-parsez--verifyorigin/r   )rF   r   )rG   rM   r+   r   _s        r    _detect_default_branchr   =  s    (BCTJJGC	 "c "yy~~b!!$  +z3EV3E3EFMM2 	MMM	8r"   c                 t    t          g d|           \  }}|r|sg S d |                                D             S )zFReturn release tags newest-first, using the repo's version-sort order.)tagz--listzv*z--sort=-v:refnamec                 ^    g | ]*}|                                 |                                 +S r$   r   .0lines     r    
<listcomp>z!_release_tags.<locals>.<listcomp>P  s-    FFFTFDJJLLFFFr"   )rF   
splitlinesrG   rM   r+   s      r    _release_tagsr   K  sN    CCCTJJGC 3 	FFS^^%5%5FFFFr"   c                 <    t          g d|           \  }}|r|r|ndS )zAReturn the latest release tag reachable from HEAD, if one exists.)rS   rT   z
--abbrev=0NrL   r   s      r    _current_release_tagr   S  s0    ;;;TBBGC&&33$&r"   c                 L    |r||k    rdS || v r|                      |          S dS )zECount release tags between current and latest in a newest-first list.r   r)   )index)tagscurrentlatests      r    _release_gapr   Y  s9     W&&q$zz'"""1r"   c                 b    |sdS t          g d|           \  }}t          |o|o||k              S )u  Return True when HEAD has moved past the latest reachable release tag.

    `git describe --tags --always` returns the bare tag name (e.g. ``v2026.5.16``)
    when HEAD is exactly on the tag, and a ``v2026.5.16-608-g1d22b9c2`` suffix
    when HEAD has moved 608 commits past it. Used by both the update check and
    the update apply path so they agree on which ref to advance to — see #2653
    (check side) and #2846 (apply side).
    FrR   )rF   bool)rG   current_tag	full_descr+   s       r    _head_is_past_latest_tagr   b  sH      u???FFMIr=y=Y+%=>>>r"   c                    t          |           }|r@|d         }t          |           }t          |||          }|dk    rt          | |          s|S t	          g d|           \  }}|r|r|S t          |           }d| S )uD  Return the same remote ref family that the update check reports.

    The update banner prefers published release tags when they exist. Applying
    an update must therefore advance to the latest release tag too; otherwise a
    checkout on a local/fork tracking branch can report release updates, pull a
    different branch that is already current, restart, and still remain behind.

    When HEAD is past the latest tag (the agent repo's day-to-day state between
    tagged releases), the check side falls through to the branch comparison via
    `_check_repo_release` returning None. The apply side must mirror that
    decision — otherwise we run `git pull --ff-only <latest-tag>` against a
    checkout that's already past the tag, no-op, restart, and the banner
    re-appears with the same N commits available. See #2846.
    r   r   z--abbrev-refz@{upstream}r   )r   r   r   r   rF   r   )rG   r   
latest_tagr   behindupstreamr+   r   s           r    _select_apply_compare_refr   q  s     D !W
*400dK<< ! 8{ K KHHH$OOLHb	 h #D))FVr"   c           
         t          |           }|sdS |d         }t          |           }t          |||          }|dk    rt          | |          rdS t	          g d|           \  }}t          |          }||||||d||d	S )z?Check if a git repo is behind its latest published release tag.Nr   r   zget-urloriginT)	namer   r   r   r   r   release_basedcurrent_versionlatest_version)r   r   r   r   rF   r   )rG   r   r   r   r   r   r   r   s           r    _check_repo_releaser     s    D taJ&t,,K$Z88F {{/kBB{t<<<dCCMJ&z22J  # &$  r"   fetchc                @   |rt          g d| d          \  }}|s|dddS t          g d|           \  }}|r|r|}nt          |           }d| }t          d	d
d| g|           \  }	}|r#|	                                rt          |	          nd}
t          dd|g|           \  }}|r!|rt          dd|g|           \  }}|r|r|nd}nd}t          dd|g|           \  }}t          g d|           \  }}t	          |          }||
||||t          |||          dS )z<Fallback: check if a git repo is behind its upstream branch.)r   r   rI      rK   r   fetch failed)r   r   r   r   r   zrev-listz--countzHEAD..z
merge-baserJ   r   z--shortNr   )r   r   r   r   r   r   compare_url)rF   r   isdigitintr   r   )rG   r   r   r   fetch_okr   r+   compare_refr   rM   r   mb_fullmb_okshortr   r   r   s                    r    _check_repo_branchr     s     H===tRPPP8 	H AGGG HHH$OOLHb	 )h )'--((( 
I/E/E/EFMMGC44SXXX1F2 |V[A4HHNGU  k9g>EE	r353%%t+y+>EEIFA <<<dCCMJ&z22J )*gvFF  r"   c                 N   | | dz                                   sdS t          g d| d          \  }}|sLt          | |          }d}|r| dt          |           }|t	          |          }||d<   d	|d
<   |S |d|d	dS t          | |          }||S t          | |d          S )zGCheck if a git repo is behind its latest release. Returns dict or None.Nr   )r   r   rT   --forcer   rK   r   : r   Tstale_check)r   r   r   r   Fr   )r]   rF   r   r!   r   r   )rG   r   	fetch_outr   release_infor,   s         r    _check_repor     s    |D6M1133|t ##K#K#KT[]^^^Ix 
*466  	J II$<Y$G$GIIG#--L$+L!*.L'	
 
 	
 'tT22LdD6666r"   c                      ddddS )zMReturn a stable update-check payload for intentionally ignored Agent updates.r   r   T)r   r   ignoredr$   r$   r"   r    _ignored_agent_update_infor   -  s    qT:::r"   )r   c                   t          |          }t          5  | sht                              d          |k    rJt	          j                    t          d         z
  t
          k     r t          t                    cddd           S t          r t          t                    cddd           S daddd           n# 1 swxY w Y   	 t          t          d          }|rt          t          d          nt                      }t          5  |t          d<   |t          d<   t	          j                    t          d<   |t          d<   t          t                    cddd           daS # 1 swxY w Y   	 dadS # daw xY w)z6Return cached update status for webui and agent repos.r   r   NTr
   r   F)r   _cache_lock_update_cacheru   time	CACHE_TTLr   _check_in_progressr   r   r	   r   )forcer   
webui_info
agent_infos       r    check_for_updatesr  2  s    ''M	 	" 	"	'!!/22mCC	mL99IEE&&	" 	" 	" 	" 	" 	" 	" 	"  	'&&	" 	" 	" 	" 	" 	" 	" 	" "	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	" 	"# G44
9Fh[W555LfLhLh
 	' 	'%/M'"%/M'"*.)++M,'-:M/*&&	' 	' 	' 	' 	' 	' 	' #	' 	' 	' 	' 	' 	' 	' 	' 	' #U""""sP   AB7B7)B77B;>B;AE0 AE E0  E$$E0 'E$(E0 0E4c                 :    | dk    rt           S | dk    rt          S d S )Nr
   r   )r   r	   )r&   s    r    _repo_path_for_update_targetr  P  s)    4r"      infoc                .    t          | |          \  }}|S )zHReturn commit subjects for an update range, if the local git refs exist.r   )&_commit_subjects_for_update_with_limit)r  r   subjects
_truncateds       r    _commit_subjects_for_updater  X  s    A$eTTTHjOr"   c                &   t          | t                    sg dfS |                     d          }|dvr-|                     dd                              d          rdn|}t	          |          }|$t          |          d	z                                  sg dfS t          |                     d
          pd                                          }t          |                     d          pd                                          }|r|sg dfS t          dt          |                    dz   }t          dd| d| d| g|d          \  }}|r|sg dfS d |                                D             }	t          |	          |k    }
|	d|         |
fS )zEReturn recent commit subjects plus whether the local list was capped.Fr   r
   r   r   r   zhermes-webuir
   Nr   r   r   r)   logz--format=%sz..z-nrO   rK   c                 ^    g | ]*}|                                 |                                 +S r$   r   r   s     r    r   z:_commit_subjects_for_update_with_limit.<locals>.<listcomp>p  s-    JJJTZZ\\J

JJJr"   )r~   r   ru   rv   r  r   r]   r   r   maxr   rF   r   r   )r  r   r&   rG   r   r   probe_limitrM   r+   r
  	truncateds              r    r	  r	  ^  s   dD!! 5yXXfF''' HHZ44==nMMYSY'//D|DJJ/7799|5y$((=))/R006688G,''-2..4466F  5yaU$$q(K}.D.DF.D.DFX;FXFXY[_ijkkkGC S 5yJJ)9)9JJJHH%IFUFY&&r"   updatesdetailsc           
         g }|D ]|}|                     |                    d          |                    d          |                    d          |                    d          |                    d          d           }t          j        |dd	          }t	          j        |                    d
                                                    S )z7Stable key for the exact update range being summarized.r   r   r   r   r   )r   r   r   r   r   T),r   )	sort_keys
separatorsrY   )appendru   r   dumpshashlibsha256encode	hexdigest)r  r  ry   itemblobs        r    _summary_cache_keyr"  u  s    G  HHV$$hhx((88M22((<0088M22
 
 	 	 	 	 :g*EEED>$++g..//99;;;r"   r   c                     t          j        ddt          | pd                                                    } t          j        dd|           } | sdS | d         dvr| dz  } | d d         S )	Nu   ^\s*(?:[-*•]+|\d+[.)])\s*r   z\s+r9   r   z.!?.   )r^   r   r   r   )r   s    r    _clean_summary_bulletr&    sr    60"c$*"ooFFLLNND6&#t$$D rBxu:r"   c                     t          | pd                                          }t          j        d|t          j                  }|sd |fS |                    d                                          }|dv rdnd}||                    d          fS )Nr   u   ^\s*(?:[-*•]+|\d+[.)])?\s*(notice|what you(?:ll|\'ll| will) notice|user(?:s)? will notice|worth knowing|worth|note)\s*:\s*(.+)$r)   >   worth knowingnoteworthr*  notice   )r   r   r^   matchIra   lower)r   rw   r-  labelcategorys        r    _split_summary_categoryr2    s    
djb//


!
!CH  Z  \_  ac  ae  f  fE SyKKNN  ""E#EEEww8HU[[^^##r"   itemsc                     t                      }g }| D ]U}t          |          }|                                }|r.||vr*|                    |           |                    |           V|S N)setr&  r/  r  add)r3  seenbulletsr   cleanedr   s         r    _unique_summary_bulletsr;    sp    55DG  '--mmoo 	s$NN7###HHSMMMNr"   r5   fallback_itemsc                   t          | pd                                          }g }|                                D ]:}t          |          \  }}t	          |          }|r|                    |           ;t          |          dk    r-|r+d t          j        d|          D             }d |D             }|sd |D             }t          |          }|pdgS )Nr   r)   c                 ,    g | ]}t          |          S r$   r&  )r   parts     r    r   z._summary_bullets_from_text.<locals>.<listcomp>  s!    ^^^d+D11^^^r"   z(?<=[.!?])\s+c                     g | ]}||S r$   r$   r   r   s     r    r   z._summary_bullets_from_text.<locals>.<listcomp>  s    :::tT:d:::r"   c                 ,    g | ]}t          |          S r$   r?  rB  s     r    r   z._summary_bullets_from_text.<locals>.<listcomp>  s!    MMMd+D11MMMr"   Updates are available.)
r   r   r   r2  r&  r  r   r^   r   r;  )	r5   r<  rw   
candidatesr   	_categorybodyr:  r9  s	            r    _summary_bullets_from_textrH    s    
djb//


!
!CJ   ' '1$77	4'-- 	'g&&&
:!^^bhGWY\>]>]^^^
::z:::
 NMMnMMM
%j11G0/00r"   c                    g }g }t          | pd                                          D ]}t          |          \  }}|dk    r|                    |           0|dk    r|                    |           Lt	          j        dt          |pd                    r|                    |           t          |          t          |          fS )Nr   r+  r*  u7   ^\s*(?:[-*•]+|\d+[.)])?\s*[A-Za-z][A-Za-z ]{1,32}\s*:)r   r   r2  r  r^   r-  r;  )r5   notice_itemsworth_itemsr   r1  rG  s         r    &_categorized_summary_bullets_from_textrL    s     LKDJB**,, & &066$x%%%%  t$$$$XPRUVZV`^`RaRabb 	&%%%"<002I+2V2VVVr"   c                    g }| D ]}|                     d          p|                     d          pd}|                     d          pd}|                     d          pg }|rXd                    |d d                   }|                     d	          rd
nd}|                    | d| d| d| d           |                    | d| d           |pdgS )Nr0  r   Hermesr   r   commitsz;    commits_truncatedzrecent updatesr   has z update(s), including r   r$  z update(s) available.rD  )ru   r?   r  )r  r9  r   r0  r   rO  
highlights	qualifiers           r    _fallback_update_bulletsrU    s   G 	I 	I!!ATXXf%5%5A(##(q((9%%+ 	I72A2;//J,0HH5H,I,IX((yINNebb&bb	bbU_bbbccccNNeGG&GGGHHHH0/00r"   c           	         g }d | D             }|d d         D ]}|                     d          p|                     d          pd}|                     d          pd}|                     d          p#t          |                     d	          pg           }|                    | d
| d| d           |r|S d | D             }t          |          dk    rdd                    |          z   dz   gS g S )Nc                 f    g | ].}|                     d           |                     d          ,|/S )rQ  commits_limitru   rB  s     r    r   z*_worth_knowing_bullets.<locals>.<listcomp>  s<    iii$TXX6I-J-JitxxXgOhOhiiiir"   r,  r0  r   rN  r   r   rX  rO  rR  z' updates; this summary uses the latest zL commit subjects, with the full comparison still available in the diff link.c           	         g | ]~}|                     d           |                     d          p|                     d          pd d|                     d           pd d|                     d           pddk    rdnd	 d
S )r   r0  r   rN  z (r   z updater)   r*   r   )rY  rB  s     r    r   z*_worth_knowing_bullets.<locals>.<listcomp>  s       88H88G< 0 0<H  	Q  	Q@R@R@WVW  	Q  	Qhlhphpqyhzhzh~  EF  hF  hF`c`c  LN  	Q  	Q  	Q  r"   r)   z#This summary combines updates from z and r$  )ru   r   r  r?   )r  r3  r  r   r0  r   r   targetss           r    _worth_knowing_bulletsr]    sN   Eii'iiiI"1" 
 
!!ATXXf%5%5A(##(q))KS)1D1D1J-K-K  ^  ^6  ^  ^%  ^  ^  ^	
 	
 	
 	
     G
 7||a5W8M8MMPSSTTIr"   summary_textc                 F   t          |           \  }|st          | t          |                    }d |D             fdD                                 fdt	          |          D                        d|dg}r|                    dd           g }|D ]W}|                    |d                    |                    d	 |d
         D                        |                    d           X|d                    |                                          fS )N)r<  c                 6    h | ]}|                                 S r$   r/  rB  s     r    	<setcomp>z2_format_update_summary_sections.<locals>.<setcomp>  s     999D4::<<999r"   c                 @    g | ]}|                                 v|S r$   ra  )r   r   notice_keyss     r    r   z3_format_update_summary_sections.<locals>.<listcomp>  s+    SSSD4::<<{3R3R43R3R3Rr"   c              3      K   | ]<}|                                 v|                                 d  D             v8|V  =dS )c                 6    h | ]}|                                 S r$   ra  )r   existings     r    rb  z<_format_update_summary_sections.<locals>.<genexpr>.<setcomp>  s#    CqCqCqYaHNNDTDTCqCqCqr"   Nra  )r   r   rd  rK  s     r    	<genexpr>z2_format_update_summary_sections.<locals>.<genexpr>  sd        ::<<{**tzz||CqCqepCqCqCq/q/q 	/q/q/q/q r"   zWhat you'll notice)titler3  zWorth knowingri  c              3       K   | ]	}d | V  
dS z- Nr$   rB  s     r    rh  z2_format_update_summary_sections.<locals>.<genexpr>  s(      >>T[$[[>>>>>>r"   r3  r   
)rL  rH  rU  extendr]  r  r?   r   )r^  r  rJ  sectionslinessectionrd  rK  s         @@r    _format_update_summary_sectionsrq    s    F| T TL+ r1,OghoOpOpqqq99L999KSSSSKSSSK     /88      *!	
 	
H  
($ 	
 	
 	
 E  WW%&&&>>WW-=>>>>>>RTYYu%%++----r"   c                 ,    t          d|          \  }}|S )Nr   )rq  )r  r  	_sectionssummarys       r    _fallback_update_summaryru    s    8WEEIwNr"   c                    d}g d}| D ]}|                     |d          d|d          d           |                    d          pg }|rd|                    d          r/|                     d	t          |           d
|d          d           |                    d |D                        n|                     d           |                     d           |d                    |          fS )Naa  You write human-readable release summaries for Hermes users. Focus on what the user will notice in the product. Keep it simple, specific, and short. avoid technical jargon, implementation details, SHA names, branch names, and file paths unless necessary. Return only bullets. Do not include headings, markdown tables, intro paragraphs, or closing notes.)z5Summarize these available updates as concise bullets.zkPrefix each bullet with `Notice:` for user-visible behavior changes or `Worth knowing:` for useful context.zyPut user-visible Notice bullets first and include every meaningful user-facing change from the available commit subjects.zVUse Worth knowing only for helpful context that is not a duplicate of a Notice bullet.zOUse everyday language and explain visible behavior changes, not code mechanics.zWReturn only prefixed bullets; the WebUI will add the fixed section headings separately.r   r0  r   r   z commit(s) behindrO  rQ  z- Showing latest z of z5 commit subjects; summarize trends, not every commit.c              3       K   | ]	}d | V  
dS rk  r$   )r   subjects     r    rh  z)_update_summary_prompt.<locals>.<genexpr>$  s(      DDn7nnDDDDDDr"   zF- No local commit subjects available; summarize only the update count.r   rl  )r  ru   r   rm  r?   )r  system
user_linesr   rO  s        r    _update_summary_promptr{    s3   	m   J   T']OOd8nOOOPPP((9%%+ 	hxx+,, !!G$x.   DDGDDDDDDDfggg"499Z((((r"   )r&   	use_cacher|  c                r   t          | t                    si } |dv r|nd}g }dD ]\  }}|r||k    r|                     |          }t          |t                    r(t          |                    d          pd          dk    rbd}	t	          d|i||	          \  }
}t          |                    d          pd          }||||                    d	          |                    d
          |                    d          |
|	t          |p|
o|t          |
          k              d	}|                    |           t          | |          }|rmt          5  t                              |          }|rt                              |           ddd           n# 1 swxY w Y   |rt          |          }d|d<   |S d}d}|rYt          |          rJt          |          \  }}	  |||          pd                                }|rd}n# t          $ r d}Y nw xY wt!          ||          \  }}d|||d|||d}|rwt          5  t          t                    t"          k    r$|t          vrt                              d           t          |          t          |<   ddd           n# 1 swxY w Y   |S )a  Build a human-readable What's New summary and keep regular diff comparison links.

    ``llm_callback`` receives ``(system_prompt, user_prompt)`` and returns text.
    The caller may wire that to AIAgent; this module keeps a deterministic
    fallback so the banner remains useful when no LLM provider is configured.
    Summaries are cached per exact update range so refreshes do not generate
    slightly different wording for the same available updates.
    r  N))r
   WebUI)r   Agentr   r   r  r   r   r   r   r   )	r   r0  r   r   r   r   rO  rX  rQ  Tcachedfallbackr   llmF)r+   rt  summary_sectionsgenerated_byr  	cache_keyr&   r\  )last)r~   r   ru   r   r	  r   r   r  r"  r   r   move_to_endcallabler{  r   rb   rq  _SUMMARY_CACHE_MAXpopitem)r  llm_callbackr&   r|  requested_targetr  r   r0  r  commit_limitrO  rQ  r   r   r  r  resultr  	candidatery  promptrn  rt  s                          r    summarize_update_payloadr  +  s    gt$$ !'+=!=!=vv4G>  
U 	'7 7 7{{3$%% 	TXXh-?-?-D1)E)E)J)J%KVUXLa\`Laiu%v%v%v""TXXh'',1--88M22((<0088M22)!%&7&^G<]QTU\Q]Q]H]!_!_

 

 	t"7G44I  	6 	6#''	22F 6**9555	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6 	6  	&\\F#F8MLI 8L)) /88	%ff55;BBDDI %$ 	 	 	III	7	7KKHg$$"	 	F  5 	5 	5>""&888Yn=\=\&&E&222(,VN9%	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 	5 Ms7   7FF"%F")$H HHAJ,,J03J0       @delayc                 z     ddl ddl fd}t          j        |d                                           dS )u  Re-exec this process after *delay* seconds.

    Called after a successful update so that the freshly-pulled code is
    loaded on the next request, rather than running with a mix of old and
    new Python modules in sys.modules.

    os.execv() replaces the current process image with a fresh interpreter
    running the same argv — sessions are preserved on disk, the HTTP port
    is reclaimed within the delay window, and the client's own
    ``setTimeout(() => location.reload(), 2500)`` lands after the restart.

    Coordinates with ``_apply_lock``: when the user updates both webui
    and agent, the client POSTs them sequentially.  Without coordination
    the restart timer scheduled by the first update's success would fire
    while the second update's git-pull is still running, killing it mid-
    stream and leaving the second repo in an unknown partial state.
    Blocking on ``_apply_lock`` before ``os.execv`` means a pending
    second update always completes before the restart happens.
    r   Nc                      dd l }  | j                   t          5  	  j        j        j        gj        z              n # t          $ r  j        d           Y nw xY wd d d            d S # 1 swxY w Y   d S )Nr   )r   sleep_apply_lockexecv
executableargvrb   _exit)r   r  rs   syss    r    _doz_schedule_restart.<locals>._do  s    
5  	 	#.)9CH)DEEEE    	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s4   A1$AA1A!A1 A!!A11A58A5T)r&   daemon)rs   r  	threadingThreadstart)r  r  rs   r  s   ` @@r    _schedule_restartr  x  sf    ( IIIJJJ      & C---3355555r"   c                    t                      }|rt          | |          S t                              d          sdddS 	 | dk    rt          }n/| dk    rt
          }n!dd|  dt                                           S ||d	z                                  sdd
dt                                           S t          g d|d          \  }}|sdddt                                           S t          |          }t          ddg|           t          dd|g|          \  }}|s"dd| ddt                                           S t          5  dt          d<   ddd           n# 1 swxY w Y   t                       d|  d| | ddt                                           S # t                                           w xY w)u  Force-reset the target repo to the latest remote HEAD.

    Unlike apply_update() which requires a clean working tree and refuses
    merge conflicts, this discards all local modifications (checkout .) and
    resets to origin/<branch> — equivalent to what the diverged/conflict
    error messages ask the user to run manually.

    Should only be called when apply_update() has already returned a
    response with ``conflict: True`` or ``diverged: True`` and the user
    has confirmed they want to discard local changes.
    FblockingUpdate already in progressr+   r,   r
   r   Unknown target: Nr   Not a git repositoryr   r   rI   rT   r   r   rK   z=Could not reach the remote repository. Check your connection.checkoutr$  resetz--hardzForce reset to z failedr   r   Tz force-updated to r+   r,   r&   restart_scheduled)r%   r/   r  acquirer   r	   releaser]   rF   r   r   r   r  )r&   r'   rG   r   r   r   r+   s          r    apply_force_updater    sy    *++N A(@@@.. F(DEEE)WDDwDD,Gv,G,GHHF 	C <v5577<,BCC@ 	7 RRRTXbdeee8 	Z 2 	) 055 	*c"D)))'8[94@@2 	T,Rk,R,R,RSS 	  	, 	,*+M,'	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	  AAKAA!%	
 
 	sI   #F$ ?F$ 6F$ -AF$ F$ E&F$ &E**F$ -E*.F$ $F?c                    t                      }|rt          | |          S t                              d          sdddS 	 t	          |           t                                           S # t                                           w xY w)z5Stash, pull --ff-only, pop for the given target repo.Fr  r  r  )r%   r/   r  r  _apply_update_innerr  )r&   r'   s     r    apply_updater    s    )++N A(@@@.. F(DEEE"6**s   A* *Bc                 *   | dk    rt           }n| dk    rt          }ndd|  dS ||dz                                  sdddS t          g d	|d
          \  }}|sdddS t	          |          }t          g d|          \  }}|sdd|dd          dS t          d |                                D                       r0dd|  dt          |          z   dz   t          |          z   dz   ddS d}|rt          dg|          \  }}|sdddS d}t          |          \  }	}
ddg}|	r|	                    |	|
g           n|	                    d|g           t          ||d          \  }}|s|rt          ddg|           |
                                }d|v sd|v r3dd|  d t          |          z   d!z   t          |          z   d"z   |z   dd#S d$|v sd%|v rdd|  d&t          |          z   d'z   |z   dS |                                r|                                dd(         nd)}dd*| dS |rt          ddg|          \  }}|sdd+dd,S t          5  d-t          d.<   ddd           n# 1 swxY w Y   t                       d|  d/| dd0S )1z?Inner implementation of apply_update, called under _apply_lock.r
   r   Fr  r  Nr   r  r  r   rK   zTCould not reach the remote repository. Check your internet connection and try again.)statusz--porcelainz--untracked-files=nozFailed to inspect repo status:    c              3   .   K   | ]}|d d         dv V  d S )Nr,  >   AAAUDDDUUAUDUUr$   r   s     r    rh  z&_apply_update_inner.<locals>.<genexpr>  sD       0 0 8AA 0 0 0 0 0 0r"   z
The local zX repo has unresolved merge conflicts. To reset to the latest remote version run: git -C z checkout . && git -C z pull --ff-onlyT)r+   r,   conflictstashzFailed to stash local changespullz	--ff-onlyr      popznot possible to fast-forwarddivergedzk repo has commits that are not on the remote branch, so a fast-forward update is not possible. Run: git -C z fetch origin && git -C z reset --hard )r+   r,   r  zdoes not trackzno tracking informationz@ branch has no upstream tracking branch configured. Run: git -C z branch --set-upstream-to=r   z(no output from git)zPull failed: z3Updated but stash pop failed -- manual merge needed)r+   r,   stash_conflictr   r   z updated successfullyr  )r   r	   r]   rF   r   anyr   r   r   rm  r/  r   r   r   r  )r&   rG   r   r   r   
status_out	status_okstashedr+   r   r   	pull_argspull_outpull_ok
pull_lowerrE   pop_oks                    r    r  r    s   	7		(C6(C(CDDD|D6M1133|(>??? NNNPT^`aaaKAx 
@
 
 	
 ,D11K %9994 J	  ^(\*UYVYUYJZ(\(\]]]
 0 0!,,..0 0 0 0 0 
 V   II&)  II& )::
 	
 	
 		
 G 'D))2 	M,KLLL
 '{33NFF%I 2&&)****(K0111 D"===Hg B 	-gu%t,,, ^^%%
)Z77:;S;S# # # #%(YY/2 !$D		* -== @KK
 !	 	 	 z))-F*-T-T# # # #%(YY/1MNP[\   ,4>>+;+;W!!$3$''AW(@(@(@AAA  gu-t44	6 	P"&   
 ( (&'l#( ( ( ( ( ( ( ( ( ( ( ( ( ( (  333!	  s   I22I69I6)r0   )r)   )r   )Fr5  )r  )]__doc__r  r   rs   r^   r:   r  r   urllib.errorr   urllib.requestcollectionsr   pathlibr   urllib.parser   
api.configr   r   r   r	   ImportErrorr   r  r   __annotations__Lockr   r   r  r   _GIT_DIAGNOSTIC_MAX_CHARScompiler   r   
IGNORECASEr   r   r   r!   r%   r   r/   rF   rN   rU   rf   rl   rx   objectr   floatr   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r  listr  tupler   r	  r"  r&  r2  r;  rH  rL  rU  r]  rq  ru  r{  r  r  r  r  r  r$   r"   r    <module>r     s  	 	 	   				 				                  # # # # # #       ! ! ! ! ! ! 7 7 7 7 7 7 7 7 7 7%%%%%%%   JJJ QQUVV )kmm + + +in in	 "
#PQQ 2:abb 2:Z\^\ijj  ;T   S C X[    $	c 	 	 	 	c 3 4    4 4 4 42' ' 'C ' ' ' ' 23! < < < <d
 < < < <#s # # # #L
$ 
3: 
 
 
 
#    & S4Z      u d
    ($s $ $ $ $P +*,,s , , ,**,,s , , ," " "&> > >    G G G' ' '  ? ? ?& & &R" " "J -1 D D D D DN$7 $7 $7N;D ; ; ; ;
#D # # # # #<     =?   d c 49     HJ ' ' ' ' 'eTXY\T]_cTcNd ' ' ' '.< <tDz <c < < < <     $# $%d
C*@ $ $ $ $	49 	c 	 	 	 	1S 1T#Y 14PS9 1 1 1 1"W WtCy$s)?S9T W W W W1d4j 1T#Y 1 1 1 1DJ 49    ,.# .T
 .uUYZ^U_adUdOe . . . .>d T$Z C    
)DJ )5c? ) ) ) )>GW[os G G Gd G#PT* Ghl Gx| G G G GZ*6 *6U *6T *6 *6 *6 *6Z;s ;t ; ; ; ;|  | | | | |s   A AA