
    sj~                     j   d Z ddlZddlZddlZddlZddlmZmZmZ ddl	m
Z
 ddlmZ ddlmZ  ej        e          Zeeegdf         Zeeegdf         Zeg ef         ZdZdZd	Zd
ZdZdZdddddddiidgddddZdZdeddddfZ defdZ!defd Z"d!edefd"Z#d#edefd$Z$d#edefd%Z%d#edee         fd&Z&d'edefd(Z'd!edee         fd)Z(	 	 	 	 d;d#ed*ee)         d+ee         d,e*d-ee         dee         fd.Z+dd/d0Z,	 d<d1ed#ed2ee         dee         fd3Z-	 	 	 	 d;d1ed#ed+ee         d,e*d2ee         d-ee         ddfd4Z.	 	 	 	 d;d1ed#ed+ee         d,e*d2ee         d-ee         ddfd5Z/d6edefd7Z0d1edefd8Z1	 	 	 	 	 d=d1ed#ed9ee2         d+ee         d,e*d2ee         d-ee         ddfd:Z3dS )>u  Auto-generate short session titles from the user's opening message.

Two stages, both off the critical path:

1. **Instant** — a deterministic title derived from the first user message,
   written before the model is even called. Costs nothing, cannot fail, and
   means a session is named the moment it starts instead of after the first
   turn finishes (which measured p50 151s / p90 1212s on real sessions).
2. **Upgrade** — one small-model call that replaces the derived title with a
   proper one. Runs on a cheap/fast tier, with thinking disabled and the
   response constrained to a JSON object, so there is no reasoning preamble to
   strip and nothing to parse out of prose.

Provenance (``derived`` < ``llm`` < ``user``) is enforced by the storage layer,
so stage 2 can only ever replace stage 1, and neither can replace a name the
user typed. That ordering is the industry-standard one — Codex CLI encodes the
same ``custom > ai > fallback`` precedence in its session importer.
    N)AnyCallableOptional)call_llm)LEGACY_SUMMARY_PREFIX)flatten_message_texti  0      a  You name chat sessions. Given the user's opening message, write a title that lets them find this conversation again in a list.

Rules:
- 3 to 7 words, sentence case (capitalize only the first word and proper nouns).
- Name what the user wants DONE, not that they asked a question.
- Keep technical terms, filenames, numbers, and error codes exact.
- Drop filler words: the, this, my, a, an.
- No trailing punctuation, no quotes, no tool names, no 'Title:' prefix.
- Never answer the message. Name it.
- Always produce something, even for a bare greeting.
__LANGUAGE_RULE__
Good: {"title": "Fix login button on mobile"}
Good: {"title": "Postgres connection pool exhaustion"}
Good: {"title": "Friendly greeting"}
Too vague: {"title": "Code changes"}
Too long: {"title": "Investigate and fix the issue where the login button does not respond on mobile devices"}

Reply with JSON only: {"title": "..."}z=- Write the title in the same language as the user's message.z - Write the title in {language}.json_schemasession_titleTobjecttitletypestringF)r   
propertiesrequiredadditionalProperties)namestrictschema)r   r   )
)z<command-message>z</command-message>)z<command-name>z</command-name>)z<command-args>z</command-args>)z<local-command-caveat>z</local-command-caveat>)z<local-command-stderr>z</local-command-stderr>)z<local-command-stdout>z</local-command-stdout>)z<task-notification>z</task-notification>)z<system-reminder>z</system-reminder>)z<ide_opened_file>z</ide_opened_file>)z<ide_selection>z</ide_selection>z[CONTEXT COMPACTIONz[Runtime note:z[System note:z[SYSTEM]z7[System: The active model for this chat has changed to returnc                     	 ddl m}  t           |             pi                     d          pi                     di                               dd                                                    S # t
          $ r Y dS w xY w)zDReturn configured title language, or empty string to match the user.r   load_config_readonly	auxiliarytitle_generationlanguage )hermes_cli.configr   strgetstrip	Exceptionr   s    9/home/agent/.hermes/hermes-agent/agent/title_generator.py_title_languager%      s    	::::::""$$*//<<BS#R((SR  
 
 %''		
    rrs   A-A0 0
A>=A>c                  "   	 ddl m}  ddlm}  |             }|                    d          pi                     d          pi } ||                    d          d          S # t
          $ r  t                              d	d
           Y dS w xY w)z=Return whether automatic session title generation is enabled.r   r   )is_truthy_valuer   r   enabledT)defaultz'Failed to read title_generation.enabledexc_info)r   r   utilsr'   r!   r#   loggerdebug)r   r'   configtitle_configs       r$   _auto_title_enabledr1      s     	;:::::))))))%%''

;//52::;MNNTRT|//	::DIIII   >NNNtts   A!A$ $&BBtextc                    | sdS |                                  }t          t          t                    dz            D ]}|}t          D ]\  }}|                                                    |          s-|                                                    |          }|dk    r*|t          |          d                                          }nk|t          |          |                                          }||t          |          z   d                                          }|p|                                 } ||k    r n|}|S )a]  Remove leading machine-authored control wrappers, including nested ones.

    Loops so ``<command-message><command-name>/work</command-name></command-message>``
    reduces to the prose the user actually typed. Unlike a refusal check, this
    still yields usable text, so a slash-command turn gets a real title instead
    of staying untitled.
    r      N)r"   rangelen_CONTROL_WRAPPERSlower
startswithfind)	r2   current_strippedopen_tag	close_tagendinnerrests	            r$   strip_control_wrappersrD      sE     rjjllG3())A-..  #4 	 	Hi>>##..x88 ..""''	22Cbyy#CMMNN399;; Xs!2399;;c)nn 4 5 56<<>> !ME0022wEN    user_messagec                     | sdS d}	 ddl m}  ||           }n,# t          $ r t                              dd           Y nw xY w||n| }t          |          S )u  Reduce a user turn to the text worth titling.

    A ``/skill`` invocation expands into a message that embeds the whole skill
    body, so feeding it to the titler verbatim titles the session after the
    *skill's* prose — "Kick off a task in a fresh isolated git worktree" — not
    after the user's request. Reuse the canonical scaffolding parser so the
    model sees ``/work — fix the title leak`` instead, then strip any control
    wrappers left around it.
    r   Nr   )describe_skill_invocationz-Skill-scaffolding summary failed; titling rawTr*   )agent.skill_commandsrH   r#   r-   r.   rD   )rF   	describedrH   r2   s       r$   _summarize_user_messagerK      s      rIUBBBBBB--l;;		 U U UDtTTTTTU!-99<D!$'''s    &AAc                    t          | t                    r|                                 sdS t          D ],}|                                                     |          r dS -t          t          |                                                     S )zReturn whether *user_message* carries real user intent to title from.

    False for machine-authored openers (compaction handoffs, runtime notes) and
    for turns that reduce to nothing once control scaffolding is stripped.
    F)
isinstancer    r"   _MACHINE_PREFIXESlstripr:   boolrK   )rF   prefixs     r$   is_titleable_user_messagerR      s     lC(( 0B0B0D0D u#    ++F33 	55	'55;;==>>>rE   c                    t          |           }|sdS t          d |                                D             d          }|sdS d                    |                                          }t          |          t          k    rT|dt                   }|                    d          }|t          dz  k    r
|d|         }|                    d          dz   }|pdS )uz  Build an instant title from the user's message. No model, never fails.

    This is what the user sees within milliseconds of sending their first
    message. It is intentionally dumb — first meaningful line, trimmed to a
    word boundary — because its job is to beat the model to the screen, not to
    beat it on quality. The model's title replaces it moments later.
    Nc              3   f   K   | ],}|                                 |                                 V  -d S Nr"   .0lns     r$   	<genexpr>zderive_title.<locals>.<genexpr>  s7      FF288::FFFFFFFrE   r    r4   u	    ,.;:—-u   …)	rK   next
splitlinesjoinsplitr7   MAX_DERIVED_TITLE_CHARSrfindrstrip)rF   r2   linecutspaces        r$   derive_titlerf     s     #<00D t FFdoo&7&7FFFKKD t88DJJLL!!D
4yy***+++,		#*a///fuf+Czz+&&.<4rE   contentc                    | sdS |                                  }t          j        d|t          j                  }|r'|                    d                                           }	 t          j        |          }t          |t                    rBt          |	                    d          t                    r|d                                          S n# t          t          f$ r Y nw xY wt          j        d|          }|ru	 t          j        d|                    d           d                                           S # t          $ r* |                    d                                           cY S w xY w	 ddlm}  |d	|                                           }n,# t           $ r t"                              d
d           Y nw xY wt'          d |                                D             d          }|                                                    d          r|dd	                                          }|                     d                                           S )a  Pull the title out of a model response.

    The JSON schema makes the object shape the expected case, but not every
    provider honors ``response_format``; fall back through a loose JSON scan
    and finally to first-line prose so a non-compliant provider still titles.
    r   z^```(?:json)?\s*(.*?)\s*```$   r   z""title\"\s*:\s*"((?:[^"\\]|\\.)*)""r   )strip_think_blocksNz/strip_think_blocks unavailable for title outputTr*   c              3   f   K   | ],}|                                 |                                 V  -d S rU   rV   rW   s     r$   rZ   z&_extract_title_text.<locals>.<genexpr>@  s7      DDrD

DDDDDDrE   title:   "')r"   rematchDOTALLgroupjsonloadsrM   dictr!   r    
ValueError	TypeErrorsearchagent.agent_runtime_helpersrk   r#   r-   r.   r\   r]   r9   r:   )rg   rawfencedparsedrq   rk   s         r$   _extract_title_textr~     sR     r
--//CX5sBIFFF &ll1oo##%%Cfd## 	+
6::g3F3F(L(L 	+'?((***	"    I;SAAE *	*:3%++a..33344::<<< 	* 	* 	*;;q>>'')))))	*WBBBBBB  s++1133 W W WFQUVVVVVW
DDS^^%5%5DDDb
I
IC
yy{{h'' !""gmmoo99U!!###s7   #A*C C#"C#><D; ;1E/.E/3$F &G Gc                    d                     | pd                                          }|                    d                                          }|                                                    d          r|dd                                         }|                    d          }|sdS t          |          dk    r|dd	                                         d
z   }|S )zFNormalize a model-produced title, or None when nothing usable remains.r[   r   ro   rm   rn   Nz.!,;:P   M   z...)r^   r_   r"   r9   r:   rb   r7   )r2   r   s     r$   _clean_titler   F  s    HHdjb''))**EKK$$&&E{{}})) "abb	!!LL!!E t
5zzBcrc
!!##e+LrE   timeoutfailure_callbackmain_runtimeruntime_validatorc           
         t                      st                              d           dS |T	  |            st                              d           dS n,# t          $ r t                              dd           Y nw xY wt	          |           dt
                   }|                                sdS t                      }|rt          	                    |          nt          }t                              d|          }d	|d
d|d
g}		 t          d|	dd||dt          i          }
|
j        d         j        j        pd}t%          t'          |                    }|lt)          |                                          t,          k    rBt                              dt)          |                                          t,                     dS |S # t          $ r~}t                              d|           t                              dd           |:	  |d|           n,# t          $ r t                              dd           Y nw xY wY d}~dS d}~ww xY w)u  Generate a session title from the user's opening message.

    Runs on the ``title_generation`` auxiliary task, which resolves to a
    small/fast model tier. Thinking is disabled and the response is constrained
    to ``{"title": "..."}`` so there is no preamble or reasoning to strip.

    Titles come from the user's message alone — every surveyed implementation
    that titles well (Claude Code, OpenCode, Cursor, OpenClaw) does the same.
    Waiting for the assistant is what made this slow, and it bought nothing:
    the user's opening message already states the intent worth naming.

    ``failure_callback`` is invoked with ``(task, exception)`` when the
    auxiliary call raises — the caller typically wires this to
    ``AIAgent._emit_auxiliary_failure`` so the user sees a warning instead
    of silently accumulating untitled sessions.

    ``runtime_validator`` is called right before the LLM request. If it
    returns False (e.g. the user's model was switched since the background
    thread captured its runtime snapshot), the call is skipped silently —
    no request is sent, so a stale title request can't reload a model the
    runtime already unloaded (#19027).
    <Auto-title skipped: auxiliary.title_generation.enabled=falseNz:Title generation skipped: runtime validator returned Falsez*Title runtime validator raised; proceedingTr*   )r   __LANGUAGE_RULE__system)rolerg   userr   @   g333333?response_format)taskmessages
max_tokenstemperaturer   r   
extra_bodyr   r   z4Rejecting answer-shaped title output (%d words > %d)zTitle generation failed: %szTitle generation tracebacktitle generationz(Title generation failure_callback raised)r1   r-   r.   r#   rK   MAX_TITLE_INPUT_CHARSr"   r%   _LANGUAGE_RULE_PINNEDformat_LANGUAGE_RULE_MATCH_USER_TITLE_PROMPT_TEMPLATEreplacer   _TITLE_RESPONSE_FORMATchoicesmessagerg   r   r~   r7   r_   _MAX_TITLE_WORDSwarning)rF   r   r   r   r   user_snippetr   language_rulepromptr   responserg   r   es                 r$   generate_titler   U  s   :    STTTt$	V$$&& YZZZt  	V 	V 	VLLEPTLUUUUU	V +<889O:O9OPL t  H 	'$$h$777&  $++,?OOF f--L11H
'# %)+AB

 

 

 "1%-5;099:: U[[]]!3!36F!F!FLLFEKKMM""$4   4 
 
 
 	4a8881DAAA'X  !3Q7777 X X XGRVWWWWWXttttt
sT   $A &A>=A>B>G G 
I9IHI&IIIII)dedupec                     t           dd           fd}	  ||          S # t          $ r8 t           dd          }|r|  ||          }|r||k    r  ||          cY S w xY w)uN  Persist a title at *source* authority, recovering from name collisions.

    The write goes through ``set_auto_title`` (precedence check + write in one
    transaction) so a manual ``/title`` set while generation was in flight is
    never overwritten. ``ValueError`` means the name is taken by an unrelated
    session (the unique-title index); rather than leave the session untitled
    (#50537), append a ``#N`` suffix via ``get_next_title_in_lineage``.

    ``dedupe=False`` re-raises that collision instead. The derived title is the
    one write on the turn's critical path, and it is also the one that collides
    constantly — it is a slice of the user's own words, and people open sessions
    with "hi" and "help me debug this". Scanning the lineage for the next free
    "hi #N" is a widening scan, run inline, for a name the model replaces a
    second later. The background stage picks the collision back up, so nothing
    is lost by declining it here.

    Returns the title actually persisted, or None when a higher-authority
    title already held the row (nothing was written).
    set_auto_titleNc                    . |           st                               d           d S | S t          dd           }| ||           r| nd S                     |           }|du rt	          d d          | S )NsourcezDSkipping %s title: a higher-authority title already holds session %sset_auto_title_if_emptyFzsession z not found when storing title)r-   r.   getattrset_session_titleRuntimeError)	candidate	legacy_fnokauto_fn
session_db
session_idr   s      r$   _setz$_persist_session_title.<locals>._set  s    7:y@@@ !J  
 tJ(A4HH	  )	*i @ @J99dJ))*i@@;;S*SSSTTTrE   get_next_title_in_lineage)r   rw   )	r   r   r   r   r   r   next_title_fndedupedr   s	   `` `    @r$   _persist_session_titler     s    ( j"2D99G       &	tE{{   
,GNN 	.-&& 	'U**tG}}s   
* ?A,+A,r   title_callbackc                 Z   | r|sdS 	 t          |          sdS t          |          }|sdS t          | ||dd          }|r<|:	  ||d           n,# t          $ r t                              dd           Y nw xY w|S # t          $ r  t                              dd           Y dS w xY w)	a&  Write the derived title synchronously. Cheap enough to run inline.

    Returns the title written, or None when nothing was written (no usable
    text, or the session already carries a title of at least ``derived``
    authority). Never raises: a titling failure must not affect the turn.
    NderivedF)r   r   zInstant-title callback failedTr*   zInstant title failed)rR   rf   r   r#   r-   r.   )r   r   rF   r   r   	persisteds         r$   apply_instant_titler     s     Z t(66 	4\** 	4*
E)E
 
 
	  	M3My)4444 M M M<tLLLLLM   +d;;;tts?   B  B  B  A B  &A;8B  :A;;B   &B*)B*c           	      b   	 t          | ||||||           dS # t          $ r}t                              d|           t                              dd           |:	  |d|           n2# t          $ r t                              dd           Y nw xY wY d}~dS Y d}~dS Y d}~dS d}~ww xY w)	u8  Generate and store the model title for a session.

    Called on a background thread. Silently skips if:
    - session_db is None
    - the session already carries an ``llm`` or ``user`` title
    - title generation fails
    - runtime_validator returns False (model was switched)

    Never lets an exception escape: this is a daemon-thread target, and an
    escaping exception would spray a raw traceback into the user's terminal
    via the default threading excepthook. The canonical trigger is the
    post-``hermes update`` stale-module window, where this function's lazy
    imports read NEW source from disk while already-cached modules
    (``agent.portal_tags`` etc.) are still the OLD version — the resulting
    ImportError repeats on every auto-title attempt until the long-running
    process restarts.
    r   r   r   r   zeAuto-title failed (harmless; if this started after an update, restart the running Hermes process): %szAuto-title tracebackTr*   Nr   z"Auto-title failure_callback raised)_auto_title_sessionr#   r-   r   r.   )r   r   rF   r   r   r   r   r   s           r$   auto_title_sessionr     s3   4R-%)/	
 	
 	
 	
 	
 	
  R R R 	6	
 	
 	

 	+d;;;'R  !3Q7777 R R RADQQQQQR ('''''777777QQQQQQRs8    
B.9B)A+*B)+&BB)BB))B.c                    | r|sdS 	 t          | dd          }| ||          }||dk    rdS n|                     |          rdS n# t          $ r Y dS w xY wddlm}	 ddlm}
 |}	 |                     |          p|}n# t          $ r Y nw xY w |
|            |	| |           t          ||||          }d}|st          |          }d}|sdS 	 t          | |||	          }|dS t                              d
|           |<	  |||           dS # t          $ r  t                              dd           Y dS w xY wdS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)u9   Body of :func:`auto_title_session` — see its docstring.Nget_session_title_sourcer   r   )set_accounting_context)set_conversation_context)r   r   r   llmr   z Auto-generated session title: %szAuto-title callback failedTr*   z&Failed to set auto-generated title: %s)r   get_session_titler#   agent.aux_accountingr   agent.portal_tagsr   get_conversation_rootr   rf   r   r-   r.   )r   r   rF   r   r   r   r   	source_fnexisting_sourcer   r   conversation_idr   r   r   r   s                   r$   r   r   G  sl     Z 	J(BDII	 'i
33O*)/K/K))*55 	F    <;;;;;:::::: O$:::FFT*   _--- :z222)!+	  E F 	 \** 	FB*:z5QWXXX	F7CCC%Jy&11111 J J J9DIIIIIIJ &%
  B B B=qAAAAAAAAABsd   &A	 A	 	
AA)B 
BBE ,E 
D &E>E EE 
E7E22E7r   c                     t          | t                    r|                     d          dk    rdS |                     d          }t          t          |t                    r|nt          |                    S )u=  Whether a history entry is a question a person actually asked.

    Hermes persists a lot of machinery under ``role="user"`` — compaction
    handoffs, model-switch markers, background-process notices — because strict
    OpenAI-compatible providers reject a system message that isn't first.
    Counting those as turns is what made a session that merely *opened* with one
    look like it was already past the point where titling applies.

    A multimodal turn is judged on its text, so "here's a screenshot, fix the
    login" counts as the real question it is.
    r   r   Frg   )rM   rv   r!   rR   r    r   )r   rg   s     r$   _is_real_user_turnr     ss     gt$$ F(;(;v(E(Eukk)$$G$gs++N1Eg1N1N  rE   c                     t          | dd          }t          |          sdS 	 t           ||          pd                                           S # t          $ r! t
                              d|d           Y dS w xY w)u  Whether the session still carries no title of any provenance.

    Titling normally reads the opening message and nothing else, but an opener
    isn't always titleable: an image with no caption, a compaction handoff, a
    bare slash command. Those sessions stayed nameless for life — the same guard
    that stops us re-titling on every turn also stopped us ever trying again.
    This reopens the question on later turns, and only while the answer is still
    missing, so a named session asks nothing and pays nothing.

    Answers False when it can't tell: an unreadable title is not a reason to
    start spending a model call per turn.
    r   NFr   zUntitled check failed for %sTr*   )r   callabler    r"   r#   r-   r.   )r   r   getters      r$   _session_is_untitledr     s     Z!4d;;FF uvvj))/R00668888   3Z$OOOuus   ,A 'A<;A<conversation_historyc           	         | r|r|sdS t          d |pg D                       }|dk    rt          | |          sdS t          |          sdS t                      st                              d           dS t          | |||           t          j        t          | ||f||||ddd          }	|	
                                 dS )	a  Title a session from its opening message: instant, then upgraded.

    Call this at the START of a turn, before the model is invoked. The derived
    title is written inline (sub-millisecond) and the model upgrade is forked
    onto a daemon thread, so nothing here is on the critical path.

    Only acts on the session's opening exchange, and only when the message
    carries real user intent (machine-authored compaction handoffs are skipped).
    Nc              3   8   K   | ]}t          |          d V  dS )ri   N)r   )rX   ms     r$   rZ   z#maybe_auto_title.<locals>.<genexpr>  s1      ZZqDVWXDYDYZZZZZZZrE   ri   r   r   Tz
auto-title)targetargskwargsdaemonr   )sumr   rR   r1   r-   r.   r   	threadingThreadr   start)
r   r   rF   r   r   r   r   r   user_msg_countthreads
             r$   maybe_auto_titler     s	   &  Z |  ZZ%9%?RZZZZZN"6z:"N"N$\22     STTT
JnMMM!*l3 0(,!2	
 
   F LLNNNNNrE   )NNNNrU   )NNNNN)4__doc__rt   loggingrp   r   typingr   r   r   agent.auxiliary_clientr   agent.context_compressorr   agent.message_contentr   	getLogger__name__r-   r    BaseExceptionFailureCallbackTitleCallbackrP   RuntimeValidatorr   r`   r   r   r   r   r   r8   rN   r%   r1   rD   rK   rR   rf   r~   r   floatrv   r   r   r   r   r   r   r   listr    rE   r$   <module>r      sd   &   				     * * * * * * * * * * + + + + + + : : : : : : 6 6 6 6 6 6		8	$	$ C/56 #sT)* BH% 
  
    - * \ :  "VX$67 	$)	
 
	 	  & $  > $    T    "     D(# (# ( ( ( (.?C ?D ? ? ? ?s x}    6&$ &$ &$ &$ &$ &$Rs x}    "  $2648d dde_d /d 	d
   01d c]d d d dN MQ 2 2 2 2 2r /3	   ]+	
 c]   L 37.2481R 1R1R 1R /	1R
 1R ]+1R   011R 
1R 1R 1R 1Rp 37.248IB IBIB IB /	IB
 IB ]+IB   01IB 
IB IB IB IBX     *     6 ,026.248; ;; ; #4.	;
 /; ; ]+;   01; 
; ; ; ; ; ;rE   