
    `gje1                        d Z ddlmZ ddlmZmZmZmZmZ dZ	dZ
	 	 	 	 ddZddZ	 	 	 	 	 	 	 	 	 	 	 	 ddZdd	Z	 	 	 	 	 	 dd
Z	 	 	 	 	 	 ddZy)u  Boundary-aware partial compression — "summarize up to here".

Inspired by Claude Code's Rewind menu "Summarize up to here" action
(v2.1.139–v2.1.142, Week 20, May 2026):
https://code.claude.com/docs/en/whats-new/2026-w20

Hermes already has ``/compress`` (full-history compaction) and an
automatic token-budget tail-protection heuristic inside
``ContextCompressor``. What was missing is *user-chosen* boundary
control: "fold everything before this point into a summary, but keep
my most recent N exchanges exactly as they are." That is the value of
the Claude Code feature — the user decides the compression boundary
instead of leaving it to the token-budget heuristic.

This module owns the pure, side-effect-free split logic so both the
CLI (``cli.py::_manual_compress``) and the gateway
(``gateway/run.py::_handle_compress_command``) share one
implementation. The slash-command surfaces handle compression of the
*head* via the existing ``_compress_context`` pipeline (preserving all
the session-rotation / lock / memory-notify machinery) and then
re-append the verbatim *tail* returned here.

Design notes / invariants honored:

* **Role alternation.** The compressed head ends with summary/handoff
  content (assistant- or user-role, possibly a trailing todo snapshot).
  The verbatim tail must begin with a ``user`` message so the rejoined
  history keeps the user↔assistant alternation that providers validate.
  :func:`split_history_for_partial_compress` snaps the tail boundary
  backwards to the nearest ``user`` turn so the rejoin is always legal.

* **No silent context mutation.** This is a manual, user-invoked
  action. It rotates the session exactly like ``/compress`` does (via
  the caller), so the prompt-cache reset is explicit and expected, not
  silent.

* **Conservative defaults.** ``keep_last`` counts *exchanges* (a user
  turn plus its following assistant/tool turns), defaulting to 2. The
  split never compresses if doing so would leave nothing in the head.
    )annotations)AnyDictListOptionalTuple   d   c                   | xs dj                         }|s	dt        dfS |j                         }|j                  d      r|t	        d      d }|t	        d      d }|j                         }|r/|d   dk(  r't        }t	        |      dk\  rt        |d	         }d
|dfS |r&|d   dv rt	        |      dk\  rd
t        |d	         dfS |r8|d   j                  d      r$d
t        |d   j                  dd	      d	         dfS dt        |xs dfS )u  Parse the argument string after ``/compress``.

    Recognizes the boundary-aware forms:

    * ``here``            → partial compress, keep ``DEFAULT_KEEP_LAST``
    * ``here 4``          → partial compress, keep 4 exchanges
    * ``--keep 4``        → partial compress, keep 4 exchanges
    * ``up to here``      → alias for ``here`` (matches Claude Code's
                            menu label "Summarize up to here")

    Anything else is treated as a focus topic for the existing full
    ``/compress <focus>`` behavior.

    Returns ``(partial, keep_last, focus_topic)``:

    * ``partial`` — True when a boundary-aware form was requested.
    * ``keep_last`` — exchanges to preserve verbatim (only meaningful
      when ``partial`` is True).
    * ``focus_topic`` — focus string for full compression, or None.
      Always None when ``partial`` is True (the two modes are exclusive;
      a focused partial compress is not a documented Claude Code
      behavior and would muddy the UX).
     FNz
up to herezup to r   herer	      T)z--keepz-kz--keep==)stripDEFAULT_KEEP_LASTlower
startswithlensplit_coerce_keep)raw_argstextloweredtokenskeeps        N/root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/partial_compress.pyparse_partial_compress_argsr   7   s)   4 N!!#D'--jjlG ,'#h-/*CMO$]]_F &)v% v;!q	*DT4 &)//CK14D\&),d22&)&&y1\&)//#q"9!"<=tCC #T\T11    c                    d}d}g }| xs dj                         D ]2  }|j                         }|dv rd}|dk(  rd}"|j                  |       4 dj                  |      ||fS )u  Strip ``--preview``/``--dry-run``/``--aggressive`` flags from the
    argument string after ``/compress`` (or its ``/compact`` alias).

    Flags may appear anywhere and coexist with the positional forms
    (``here [N]``, ``--keep N``, or a focus topic); the returned
    remainder is what :func:`parse_partial_compress_args` should see.

    Returns ``(remaining_args, preview, aggressive_requested)``:

    * ``preview`` — True when ``--preview`` or ``--dry-run`` was given.
      The caller must report what WOULD be compressed (message counts,
      token estimate, boundary) and make **no changes**.
    * ``aggressive_requested`` — True when ``--aggressive`` was given.
      The current surfaces do not implement an LLM-free hard-truncate
      path (it would need its own transcript-persistence branch outside
      the guarded ``_compress_context`` rotation machinery), so callers
      surface a "not supported" note instead of silently treating the
      flag as a focus topic.
    Fr   )z	--previewz	--dry-runz--dryrunTz--aggressive )r   r   appendjoin)r   preview
aggressivekepttoklows         r   extract_compress_flagsr(   o   sx    ( GJDB%%' iik88GN"JKK 88D>7J..r   c                   t        |       }t        |       }g }|}|r t        | |      \  }}|sd}t        |       g }}ddt        |       d| d|ddg}	|r"|	j                  d| d	t        |       d
       n|r|	j                  d       |r|	j                  d| d       |	j                  d       t        |      t        |      |||	dS )u  Build the ``/compress --preview`` report — pure, no side effects.

    Shared by the CLI (``cli.py::_manual_compress``) and the gateway
    (``gateway/slash_commands.py::_handle_compress_command``) so both
    surfaces report the same numbers the real run would use.

    Returns a dict with ``head_count``/``tail_count``/``lines`` where
    ``lines`` is a ready-to-print list of report strings.
    Fu   Preview — no changes made.zWould compress z of z message(s) (~,z tokens currently in context).zBoundary: keeping the last z exchange(s) (z message(s)) verbatim.uR   Boundary: 'here' split would keep everything — falling back to full compression.zFocus topic: ""z1Run the command again without --preview to apply.)
head_count
tail_counttotalpartiallines)r   list"split_history_for_partial_compressr!   )
historyr/   	keep_lastfocus_topicapprox_tokensr.   headtaileffective_partialr0   s
             r   summarize_compress_previewr:      s
     LE=D!#D7K
d %g$D 	'
#d)D 01;	=E
 )) 5D	{02	
 
0	
 ~k]!45	LLDE $i$i$ r   c                ~    	 t        |       }|dk  ry|t        kD  rt        S |S # t        t        f$ r	 t        cY S w xY w)z9Parse a keep-count token, clamping to [1, MAX_KEEP_LAST].r   )int	TypeError
ValueErrorr   MAX_KEEP_LAST)valuens     r   r   r      sK    !J 	1u=H z" !  !s   $ <<c                :   |dk  rd}t        |       }|dk(  rg g fS g }t        |dz
  dd      D ];  }| |   j                  d      dk(  s|j                  |       t        |      |k\  s; n |st	        |       g fS |d   }| d| }| |d }|st	        |       g fS ||fS )uO  Split ``history`` into ``(head, tail)`` for partial compression.

    ``head`` is the earlier portion that will be summarized; ``tail`` is
    the most recent ``keep_last`` exchanges, preserved verbatim.

    An *exchange* is counted by ``user``-role messages: keeping N
    exchanges means keeping everything from the Nth-most-recent ``user``
    message onward. This guarantees the tail starts on a ``user`` turn,
    so when the caller rejoins ``compressed_head + tail`` the
    user↔assistant alternation stays valid (the compressed head's
    trailing content is followed by a fresh user turn).

    Returns ``(head, tail)``. If the split would leave the head empty
    (not enough history to compress meaningfully), returns
    ``(history, [])`` — signaling the caller to fall back to full
    compression or report "nothing to do".
    r   r   roleuserN)r   rangegetr!   r1   )r3   r4   rA   user_startsidxboundaryr7   r8   s           r   r2   r2      s    * 1}	GAAv2v  KQUB# 3<F#v-s#;9,	  G}b  2H9HD89D
 G}b  :r   c                   |st        |       S | st        |      S t        |       }t        |      }|d   }|d   }|j                  d      }|j                  d      }||k(  r|dv r|j                  d      }|j                  d      }	t        |t              r4t        |	t              r$t	        |      }
| d|	 |
d<   |
|d<   |dd }||z   S |d	k(  rd
nd	}|j                  |dd       ||z   S )u0  Concatenate a compressed head with the verbatim tail, defending
    the seam against an illegal user→user / assistant→assistant adjacency.

    In normal operation the compressed head ends with the head's own
    protected verbatim tail (the ``ContextCompressor`` always preserves a
    recent window), which terminates on an ``assistant``/``tool`` turn —
    so ``assistant → user`` at the seam is already valid. But the head
    compressor's exact output shape is not contractually guaranteed (a
    plugin context engine could return something that ends on a ``user``
    turn, or a degenerate single-summary message). Rather than trust the
    seam, this helper inspects the boundary and, if the last head message
    and the first tail message share a ``user``/``assistant`` role, folds
    the tail's first message content onto the head's last message so the
    rejoined list never violates provider role-alternation rules.

    ``tool`` messages are left alone — consecutive ``tool`` entries are
    the one legal repetition (parallel tool results).
    rC   r   rD   )rE   	assistantcontentz

r   NrE   rL   r   )rD   rM   )r1   rG   
isinstancestrdictr!   )compressed_headr8   r7   restlastfirst	last_role
first_rolelast_contentfirst_contentmergedbridge_roles               r   rejoin_compressed_head_and_tailr[     s
   , O$$Dz D:D8DGE I6"JJ90E#E xx	*		),lC(Zs-K$ZF#/.]O DF9DH8D $; *4v)=+6KKK<=$;r   N)r   rO   returnzTuple[bool, int, Optional[str]])r   rO   r\   zTuple[str, bool, bool])r3   List[Dict[str, Any]]r/   boolr4   r<   r5   zOptional[str]r6   r<   r\   zDict[str, Any])r@   rO   r\   r<   )r3   r]   r4   r<   r\   z1Tuple[List[Dict[str, Any]], List[Dict[str, Any]]])rQ   r]   r8   r]   r\   r]   )__doc__
__future__r   typingr   r   r   r   r   r   r?   r   r(   r:   r   r2   r[    r   r   <module>rc      s   'R # 3 3   5252$52p/D4!44 4 	4
 4 4n
5!55 75p7)7
7 7r   