
    NQjb*                        U d Z ddlmZ ddlZddlmZmZmZ  ej        e	          Z
dd	ZddZddZddZdZdZded<   dZddZedddZdS )ux  Replay-history sanitization shared across resume code paths.

When a session's last turn dies mid-tool-loop — the process is killed by a
restart/shutdown command, a stale-timeout fires, or an interrupt lands before
the tool result is written — the persisted transcript can end with a dangling
``assistant(tool_calls)`` (no matching ``tool`` answer) or an interrupted
``assistant→tool`` block.  On resume the model sees that broken tail and
re-issues the unanswered call, producing an endless "thinking"/reboot loop
(#49201, #29086).

These pure helpers strip those tails before the history is replayed to the
model.  They were originally local to ``gateway/run.py`` (which fixed the
messaging-gateway path) and are extracted here so every resume surface — the
messaging gateway AND the TUI/WebUI gateway — shares the same cleanup instead
of the WebUI path silently skipping it.
    )annotationsN)AnyDictListcontentr   returnboolc                    t          | t                    sdS |                                 }d|v rdS d|v rd|v sd|v rd|v S dS )z@Return True if a tool result indicates the tool was interrupted.Fz[command interrupted]T	exit_code130z-1	interrupt)
isinstancestrlower)r   lowereds     8/home/alina/.hermes/hermes-agent/agent/replay_cleanup.pyis_interrupted_tool_resultr      sc    gs## ummooG'))tg5G#3#3twg%%5    agent_historyList[Dict[str, Any]]c                    | s| S g }d}t          |           }||k     r_| |         }|                    d          dk    rd|v r|dz   }g }||k     rd| |                             d          dk    rE|                    | |                    |dz  }||k     r| |                             d          dk    E|rIt          d |D                       r0t                              d||dz
  t          |                     |}|                    d          dk    rDt          |                    d	d
                    r!t                              d           |dz  }E|                    |           |dz  }||k     _|S )u  Strip interrupted assistant→tool sequences from replay history.

    Older interrupted gateway turns can be followed by a queued real user
    message, so the interrupted assistant/tool block is not necessarily the
    final tail by the time we rebuild replay history.  Remove any contiguous
    assistant(tool_calls) + tool-result block that contains an interrupted tool
    result, while preserving successful tool-call sequences intact.
    r   role	assistant
tool_calls   toolc              3  \   K   | ]'}t          |                    d d                    V  (dS )r    N)r   get).0ms     r   	<genexpr>z/strip_interrupted_tool_tails.<locals>.<genexpr>?   sL       $ $ +155B+?+?@@$ $ $ $ $ $r   uV   Stripping interrupted assistant→tool replay block (indices %d–%d, tool_results=%d)r   r   z<Stripping orphan interrupted tool result from replay history)lenr   appendanyloggerdebugr   )r   cleanedinmsgjtool_resultss          r   strip_interrupted_tool_tailsr.   &   s     $&G	AMA
a%%A776??k))lc.A.AAA13La%%M!,0088FBB##M!$4555Q a%%M!,0088FBB  
 $ $%$ $ $ ! ! 
 9q1uc,//  
 776??f$$)CCGGIWYDZDZ)[)[$LLWXXXFAs	Q1 a%%4 Nr   c                2   | s| S | d         }t          |t                    r.|                    d          dk    r|                    d          s| S t                              dt          |                    d          pg                      | dd         S )u  Strip a trailing ``assistant(tool_calls)`` block left with NO answers.

    When a tool call itself kills the gateway process (``docker restart``,
    ``systemctl restart``, ``kill``, ``hermes gateway restart``), the process
    is terminated by SIGKILL *mid-call* — before the tool result is ever
    written and before the orderly shutdown rewind
    (``_drop_trailing_empty_response_scaffolding``) can run.  The last thing
    persisted is the ``assistant`` message that issued the ``tool_calls``,
    with zero matching ``tool`` rows.

    On resume the model sees an unanswered tool call at the tail and naturally
    re-issues it — which restarts the gateway again, producing the infinite
    reboot loop in #49201.  ``strip_interrupted_tool_tails`` does not catch
    this because there is no tool result to inspect for an interrupt marker.

    This strips that dangling tail at the source so there is nothing for the
    model to re-execute.  It only acts when the tail is an
    ``assistant(tool_calls)`` whose calls have NO corresponding ``tool``
    results — a completed assistant→tool pair (any tool answers present) is
    left untouched so genuine mid-progress tool loops still resume.
    r   r   r   u   Stripping dangling unanswered assistant(tool_calls) tail (%d call(s)) — process likely killed mid-tool-call by a restart/shutdown command (#49201)N)r   dictr   r&   r'   r#   )r   lasts     r   strip_dangling_tool_call_tailr3   T   s    0  D4HHV++HH\"" , 
LL	, 	DHH\""(b))	   "r   c                B    | s| S t          t          |                     S )uC  Apply both replay-tail strippers in the canonical order.

    Convenience entry point for resume code paths: removes interrupted
    assistant→tool blocks anywhere in the history, then removes a dangling
    unanswered ``assistant(tool_calls)`` tail.  Returns the same list object
    when there is nothing to strip.
    )r3   r.   )r   s    r   sanitize_replay_historyr5      s)      ()Em)T)TUUUr   g      N@)zconfirm forced restartzconfirm forced rebootzconfirm shutdownzconfirm rebootzconfirm power offzyes, delete everythingzconfirm wipezconfirm factory resetu   確認強制重開機u   確認強制重開u   確認重啟tuple _DANGEROUS_CONFIRMATION_PATTERNSz[A high-risk confirmation previously given here has EXPIRED and must not be acted on. Ask the user to re-confirm explicitly before performing any destructive action.]c                    t          | t                    sdS |                                                                 t	          fdt
          D                       S )a%  Return True if a user-message text matches a known dangerous confirmation.

    Used by ``strip_stale_dangerous_confirmations`` to decide which
    transcript rows to expire. Substring + case-insensitive so that
    ``"Please confirm forced restart, the host is critical"`` still matches.
    Fc              3      K   | ]}|v V  	d S )N )r    patterntexts     r   r"   z,is_dangerous_confirmation.<locals>.<genexpr>   s'      OO7w$OOOOOOr   )r   r   stripr   r%   r7   )r   r<   s    @r   is_dangerous_confirmationr>      sY     gs## u==??  ""DOOOO.NOOOOOOr   )expiry_secondsnowfloatr?   c          	     8   | s| S g }| D ]}t          |t                    r|                    d          dk    rt          |                    dd                    r|                    d          }||t	          |          z
  |k    ryt
                              d|t	          |          z
  ||                    d          pddd                    t          |          }t          |d<   |                    |           |                    |           |S )	u  Expire stale dangerous-confirmation text in user messages (#59607).

    When a high-risk side effect (e.g. host restart via ``shutdown.exe``)
    runs, the user's plain-text confirmation phrase is persisted in the
    conversation transcript.  If the host restart killed the gateway
    process before the assistant's tool result was written, the
    transcript tail ends on the assistant's text response — and the
    dangerous confirmation text remains in the user role.

    On the next inbound message — possibly a casual "are you there?" from
    the user minutes later — the LLM sees the stale confirmation and may
    interpret the new turn as a fresh re-confirmation, re-executing the
    destructive action.  This is the failure mode reported in #59607.

    Expired confirmations are REDACTED IN PLACE, not removed: deleting a
    user message from the incident tail (``user(confirm) →
    assistant("OK, restarting")``) would leave two consecutive assistant
    messages, violating the strict role-alternation invariant providers
    enforce.  The message survives with its role intact; only the trigger
    text is replaced by a sentinel that tells the model the confirmation
    has expired.

    Messages without a timestamp are left untouched (backward
    compatibility: legacy transcripts and in-memory test scaffolding have
    no timestamps).  User messages that contain dangerous confirmation
    text but are within the expiry window are also left untouched — they
    represent a fresh confirmation that has not yet been acted on.

    Complements 75ed07ace (which strips the *assistant* side of the
    broken tail) by handling the *user* side: a stale plain-text
    confirmation that the assistant has not yet responded to in a way
    the resume logic recognises.
    r   userr   r   	timestampNzYRedacting stale dangerous-confirmation text in user message (age=%.1fs, expiry=%.1fs): %rP   )	r   r1   r   r>   rA   r&   r'   _EXPIRED_CONFIRMATION_SENTINELr$   )r   r@   r?   r(   r+   tsredacteds          r   #strip_stale_dangerous_confirmationsrI      s&   N  $&G  sD!!	6)))#'')R*@*@AA * %%B~3r?n"D"D<%))O"WWY''-2ss3    99&D#x(((sNr   )r   r   r   r	   )r   r   r   r   )r   r   r@   rA   r?   rA   r   r   )__doc__
__future__r   loggingtypingr   r   r   	getLogger__name__r&   r   r.   r3   r5   &_DANGEROUS_CONFIRMATION_EXPIRY_SECONDSr7   __annotations__rF   r>   rI   r:   r   r   <module>rR      s+    " # " " " " "  " " " " " " " " " "		8	$	$	 	 	 	+ + + +\) ) ) )XV V V V, *. &+      &* 
P 
P 
P 
P" C	? ? ? ? ? ? ? ?r   