
    i              	       .   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mZ ddl	m	Z	m
Z
 ddlmZ ddlmZmZmZmZ  ej        e          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dlmZmZmZmZ e G d d                      Ze G d d                      Z  e!ej"        ej#        ej$        ej%        h          Z&	 ddde de'defdZ(e G d d                      Z)	 	 d$dede'de'defdZ* G d d           Z+	 d%ded!ed"ee)         de fd#Z,dS )&a  
Session management for the gateway.

Handles:
- Session context tracking (where messages come from)
- Session storage (conversations persisted to disk)
- Reset policy evaluation (when to start fresh)
- Dynamic system prompt injection (agent knows its context)
    N)Path)datetime	timedelta)	dataclass)DictListOptionalAnyreturnc                  (    t          j                    S )zReturn the current local time.)r   now     7/home/agentuser/.hermes/hermes-agent/gateway/session.py_nowr      s    <>>r   valuec                     t          j        |                     d                                                    dd         S )z0Deterministic 12-char hex hash of an identifier.utf-8N   )hashlibsha256encode	hexdigestr   s    r   _hash_idr   "   s3    >%,,w//00::<<SbSAAr   c                 &    dt          |            S )z%Hash a sender ID to ``user_<12hex>``.user_)r   r   s    r   _hash_sender_idr   '   s    $8E??$$$r   c                     |                      d          }|dk    r)| d|         }| dt          | |dz   d                    S t          |           S )u   Hash the numeric portion of a chat ID, preserving platform prefix.

    ``telegram:12345`` → ``telegram:<hash>``
    ``12345``          → ``<hash>``
    :r   N   )findr   )r   colonprefixs      r   _hash_chat_idr%   ,   s^     JJsOOEqyyvv888E%!)**$566888E??r   r!   )PlatformGatewayConfigSessionResetPolicyHomeChannelc                   T   e Zd ZU dZeed<   eed<   dZee         ed<   dZ	eed<   dZ
ee         ed<   dZee         ed	<   dZee         ed
<   dZee         ed<   dZee         ed<   dZee         ed<   edefd            Zdeeef         fdZedeeef         dd fd            ZdS )SessionSourcez
    Describes where a message originated from.
    
    This information is used to:
    1. Route responses back to the right place
    2. Inject context into the system prompt
    3. Track origin for cron job delivery
    platformchat_idN	chat_namedm	chat_typeuser_id	user_name	thread_id
chat_topicuser_id_altchat_id_altr   c                    | j         t          j        k    rdS g }| j        dk    r'|                    d| j        p| j        pd            n| j        dk    r%|                    d| j        p| j                    nQ| j        dk    r%|                    d| j        p| j                    n!|                    | j        p| j                   | j	        r|                    d	| j	                    d

                    |          S )z)Human-readable description of the source.zCLI terminalr/   DM with usergroupgroup: channel	channel: zthread: , )r,   r&   LOCALr0   appendr2   r1   r.   r-   r3   join)selfpartss     r   descriptionzSessionSource.descriptionV   s    =HN**!>>T!!LLNDN$Ldl$LfNNOOOO^w&&LLC4>#AT\CCDDDD^y((LLET^%Ct|EEFFFFLL74<888> 	6LL4DN44555yyr   c           	          | j         j        | j        | j        | j        | j        | j        | j        | j        d}| j	        r
| j	        |d<   | j
        r
| j
        |d<   |S )N)r,   r-   r.   r0   r1   r2   r3   r4   r5   r6   )r,   r   r-   r.   r0   r1   r2   r3   r4   r5   r6   )rB   ds     r   to_dictzSessionSource.to_dictk   sp    +||/	
 	
  	0#/Am 	0#/Amr   datac                     | t          |d                   t          |d                   |                    d          |                    dd          |                    d          |                    d          |                    d          |                    d	          |                    d
          |                    d          
  
        S )Nr,   r-   r.   r0   r/   r1   r2   r3   r4   r5   r6   )
r,   r-   r.   r0   r1   r2   r3   r4   r5   r6   )r&   strget)clsrH   s     r   	from_dictzSessionSource.from_dict|   s    sd:.//Y((hh{++hh{D11HHY''hh{++hh{++xx--////
 
 
 	
r   )__name__
__module____qualname____doc__r&   __annotations__rJ   r.   r	   r0   r1   r2   r3   r4   r5   r6   propertyrD   r   r
   rG   classmethodrM   r   r   r   r+   r+   A   sQ          LLL#Ix}###Is!GXc]!!!#Ix}####Ix}### $J$$$!%K#%%%!%K#%%% S       X (c3h    " 
T#s(^ 
 
 
 
 [
 
 
r   r+   c                       e Zd ZU dZeed<   ee         ed<   eee	f         ed<   dZ
eed<   dZeed<   dZee         ed	<   dZee         ed
<   deeef         fdZdS )SessionContexta  
    Full context for a session, used for dynamic system prompt injection.
    
    The agent receives this information to understand:
    - Where messages are coming from
    - What platforms are available
    - Where it can deliver scheduled task outputs
    sourceconnected_platformshome_channels session_key
session_idN
created_at
updated_atr   c                 8   | j                                         d | j        D             d | j                                        D             | j        | j        | j        r| j                                        nd | j	        r| j	                                        nd dS )Nc                     g | ]	}|j         
S r   r   ).0ps     r   
<listcomp>z*SessionContext.to_dict.<locals>.<listcomp>   s    #N#N#NAG#N#N#Nr   c                 H    i | ]\  }}|j         |                                 S r   )r   rG   )ra   rb   hcs      r   
<dictcomp>z*SessionContext.to_dict.<locals>.<dictcomp>   s5       */!R  r   )rW   rX   rY   r[   r\   r]   r^   )
rW   rG   rX   rY   itemsr[   r\   r]   	isoformatr^   rB   s    r   rG   zSessionContext.to_dict   s    k))++#N#NT5M#N#N#N 373E3K3K3M3M    +/9=R$/33555d9=R$/33555d

 

 
	
r   )rN   rO   rP   rQ   r+   rR   r   r&   r   r)   r[   rJ   r\   r]   r	   r   r^   r
   rG   r   r   r   rV   rV      s           h'''+-.... KJ%)J")))%)J")))
c3h 
 
 
 
 
 
r   rV   F)
redact_piicontextrj   c          
      r	   |o| j         j        t          v }ddg}| j         j        j                                        }| j         j        t
          j        k    r|                    d| d           n| j         }|ru|j        p|j	        rt          |j	                  nd}|j        pt          |j                  }|j        dk    rd| }n,|j        dk    rd	| }n|j        d
k    rd| }n
|}n|j        }|                    d| d| d           | j         j        r"|                    d| j         j                    | j         j        dk    o| j         j        }|r|                    d           np| j         j        r#|                    d| j         j                    nA| j         j	        r5| j         j	        }	|rt          |	          }	|                    d|	            | j         j        t
          j        k    r+|                    d           |                    d           nD| j         j        t
          j        k    r*|                    d           |                    d           dg}
| j        D ]/}|t
          j        k    r|
                    |j         d           0|                    dd                    |
                      | j        r|                    d           |                    d           | j                                        D ]K\  }}|rt          |j                  n|j        }|                    d|j         d|j         d| d           L|                    d           |                    d           ddlm} | j         j        t
          j        k    r|                    d           nL| j         j        p&|rt          | j         j                  n| j         j        }|                    d | d           |                    d! |             d"           | j                                        D ]+\  }}|                    d#|j         d$|j         d           ,|                    d           |                    d%           d&                    |          S )'a}  
    Build the dynamic system prompt section that tells the agent about its context.
    
    This is injected into the system prompt so the agent knows:
    - Where messages are coming from
    - What platforms are connected
    - Where it can deliver scheduled task outputs

    When *redact_pii* is True **and** the source platform is in
    ``_PII_SAFE_PLATFORMS``, phone numbers are stripped and user/chat IDs
    are replaced with deterministic hashes before being sent to the LLM.
    Platforms like Discord are excluded because mentions need real IDs.
    Routing still uses the original values (they stay in SessionSource).
    z## Current Session ContextrZ   z**Source:** z! (the machine running this agent)r9   r/   r8   r:   r;   r<   r=   z ()z**Channel Topic:** uq   **Session type:** Multi-user thread — messages are prefixed with [sender name]. Multiple users may participate.z
**User:** z**User ID:** u?  **Platform notes:** You are running inside Slack. You do NOT have access to Slack-specific APIs — you cannot search channel history, pin/unpin messages, manage channels, or list users. Do not promise to perform these actions. If the user asks, explain that you can only read messages sent directly to you and respond.uC  **Platform notes:** You are running inside Discord. You do NOT have access to Discord-specific APIs — you cannot search channel history, pin messages, manage roles, or list server members. Do not promise to perform these actions. If the user asks, explain that you can only read messages sent directly to you and respond.zlocal (files on this machine)u   : Connected ✓z**Connected Platforms:** r>   z)**Home Channels (default destinations):**z  - z: z (ID: z)**Delivery options for scheduled tasks:**r   )display_hermes_homeu.   - `"origin"` → Local output (saved to files)u$   - `"origin"` → Back to this chat (u*   - `"local"` → Save to local files only (z/cron/output/)z- `"u   "` → Home channel (zb*For explicit targeting, use `"platform:chat_id"` format if the user provides a specific chat ID.*
)rW   r,   _PII_SAFE_PLATFORMSr   titler&   r?   r@   r2   r1   r   r.   r%   r-   r0   rD   r4   r3   SLACKDISCORDrX   rA   rY   rg   namehermes_constantsrn   )rk   rj   linesplatform_namesrc_uname_cnamedesc_is_shared_threaduidplatforms_listrb   r,   homehc_idrn   _origin_labels                    r   build_session_context_promptr      sS   ( N 7;N NJ$
E N+17799M~(.00TMTTTUUUU n 	#] 03G,,,  ]@mCK&@&@F}$$*&**'))))))+++6++?D<M<<T<<<=== ~  HF7>+DFFGGG 	 D( 	%N$   ,B	
 	
 	
 	
 
	! ,<'.":<<====		 ,n$ 	'!#&&C*S**+++ ~(.00RP	
 	
 	
 	
 
	 H$4	4	4RP	
 	
 	
 66N( ? ?!!QW"="="=>>>	LLHTYY~-F-FHHIII  MR@AAA%399;; 	M 	MNHd3=OM$,///4<ELLKKK$)KK5KKKLLLL 
LL	LL<===444444 ~(.00GHHHH0 
5?[M'.0111W^E[ 	 	NmNNNOOO 
LL\7J7J7L7L\\\  
 "/5577 Q Q$OX^OO49OOOPPPP 
LL	LLwxxx99Ur   c                      e Zd ZU dZeed<   eed<   eed<   eed<   dZee	         ed<   dZ
ee         ed<   dZee         ed	<   d
Zeed<   dZeed<   dZeed<   dZeed<   dZeed<   dZeed<   dZeed<   dZeed<   dZeed<   dZeed<   dZee         ed<   dZeed<   dZeed<   dZeed<   deeef         fdZ e!deeef         dd fd             Z"dS )!SessionEntryzi
    Entry in the session store.
    
    Maps a session key to its current session ID and metadata.
    r[   r\   r]   r^   Norigindisplay_namer,   r/   r0   r   input_tokensoutput_tokenscache_read_tokenscache_write_tokenstotal_tokens        estimated_cost_usdunknowncost_statuslast_prompt_tokensFwas_auto_resetauto_reset_reasonreset_had_activitymemory_flushed	suspendedr   c                    i d| j         d| j        d| j                                        d| j                                        d| j        d| j        r| j        j        nd d| j        d| j	        d	| j
        d
| j        d| j        d| j        d| j        d| j        d| j        d| j        d| j        }| j        r| j                                        |d<   |S )Nr[   r\   r]   r^   r   r,   r0   r   r   r   r   r   r   r   r   r   r   r   )r[   r\   r]   rh   r^   r   r,   r   r0   r   r   r   r   r   r   r   r   r   r   r   rG   )rB   results     r   rG   zSessionEntry.to_dict|  sN   
4+
$/
 $/3355
 $/3355	

 D-
 t}F++$
 
 D-
 T/
  !7
 !$"9
 D-
 !$"9
 !$"9
 4+
  d1!
" #
& ; 	5#{2244F8r   rH   c           	         d }d|v r(|d         r t                               |d                   }d }|                    d          rP	 t          |d                   }n9# t          $ r,}t
                              d|d         |           Y d }~nd }~ww xY w | di d|d         d|d         dt          j        |d                   dt          j        |d                   d|d|                    d          d|d	|                    d	d
          d|                    dd          d|                    dd          d|                    dd          d|                    dd          d|                    dd          d|                    dd          d|                    dd          d|                    dd          d|                    dd          d|                    dd          S )Nr   r,   zUnknown platform value %r: %sr[   r\   r]   r^   r   r0   r/   r   r   r   r   r   r   r   r   r   r   r   r   Fr   r   )	r+   rM   rK   r&   
ValueErrorloggerdebugr   fromisoformat)rL   rH   r   r,   es        r   rM   zSessionEntry.from_dict  s   tX",,T(^<<F88J 	SS#D$455 S S S<d:>NPQRRRRRRRRS s 
 
 
]++
L))
  -d<.@AAA
  -d<.@AAA	

 6
 .111
 X
 hh{D111
 .!444
 ((?A666
 #hh':A>>>
  $xx(<a@@@
 .!444
  $xx(<a@@@
  $xx(<cBBB
  	:::!
"  88$4e<<<#
$ hh{E222%
 	
s   A 
B'"BB)#rN   rO   rP   rQ   rJ   rR   r   r   r	   r+   r   r,   r&   r0   r   intr   r   r   r   r   floatr   r   r   boolr   r   r   r   r   r
   rG   rT   rM   r   r   r   r   r   K  s         
 OOO '+FH]#*** #'L(3-&&&#'Hhx '''Is L#M3sL# #### K      !ND   '+x}+++$$$$ !ND   
 Itc3h    0 
T#s(^ 
 
 
 
 [
 
 
r   r   TrW   group_sessions_per_userthread_sessions_per_userc                    | j         j        }| j        dk    rJ| j        r)| j        rd| d| j         d| j         S d| d| j         S | j        rd| d| j         S d| dS | j        p| j        }d|| j        g}| j        r|                    | j                   | j        r|                    | j                   |}| j        r|sd}|r$|r"|                    t          |                     d	                    |          S )u  Build a deterministic session key from a message source.

    This is the single source of truth for session key construction.

    DM rules:
      - DMs include chat_id when present, so each private conversation is isolated.
      - thread_id further differentiates threaded DMs within the same DM chat.
      - Without chat_id, thread_id is used as a best-effort fallback.
      - Without thread_id or chat_id, DMs share a single session.

    Group/channel rules:
      - chat_id identifies the parent group/channel.
      - user_id/user_id_alt isolates participants within that parent chat when available when
        ``group_sessions_per_user`` is enabled.
      - thread_id differentiates threads within that parent chat.  When
        ``thread_sessions_per_user`` is False (default), threads are *shared* across all
        participants — user_id is NOT appended, so every user in the thread
        shares a single session.  This is the expected UX for threaded
        conversations (Telegram forum topics, Discord threads, Slack threads).
      - Without participant identifiers, or when isolation is disabled, messages fall back to one
        shared session per chat.
      - Without identifiers, messages fall back to one session per platform/chat_type.
    r/   zagent:main:z:dm:r    z:dmz
agent:mainF)
r,   r   r0   r-   r3   r5   r1   r@   rJ   rA   )rW   r   r   r,   participant_id	key_partsisolate_users          r   build_session_keyr     s`   8 $H4> 	@ WVXVV6>VVFDTVVV???v~??? 	BAAAv/?AAA*X****'96>Nx)9:I~ )((( +)***
 +L  8  . .^,,---88Ir   c            	          e Zd ZdZ	 d'dedefdZd(dZd(dZd(d	Z	d
e
defdZdedefdZded
e
dee         fdZdefdZ	 d)d
e
dedefdZ	 d'dededdfdZdedefdZd*dedefdZdedee         fdZdededee         fdZd'dee         dee         fdZdedefd Zd)ded!eeef         d"eddfd#Zded$eeeef                  ddfd%Z dedeeeef                  fd&Z!dS )+SessionStorez
    Manages session storage and retrieval.
    
    Uses SQLite (via SessionDB) for session metadata and message transcripts.
    Falls back to legacy JSONL files if SQLite is unavailable.
    Nsessions_dirconfigc                 
   || _         || _        i | _        d| _        t	          j                    | _        || _        d | _        	 ddl	m
}  |            | _        d S # t          $ r}t          d|            Y d }~d S d }~ww xY w)NFr   )	SessionDBzL[gateway] Warning: SQLite session store unavailable, falling back to JSONL: )r   r   _entries_loaded	threadingLock_lock_has_active_processes_fn_dbhermes_stater   	Exceptionprint)rB   r   r   has_active_processes_fnr   r   s         r   __init__zSessionStore.__init__  s    (13^%%
(?% 	f...... y{{DHHH 	f 	f 	fdabddeeeeeeeee	fs   A 
B%A==Br   c                 n    | j         5  |                                  ddd           dS # 1 swxY w Y   dS )z4Load sessions index from disk if not already loaded.N)r   _ensure_loaded_lockedri   s    r   _ensure_loadedzSessionStore._ensure_loaded  s    Z 	) 	)&&(((	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	)s   *..c                    | j         rdS | j                            dd           | j        dz  }|                                r	 t	          |dd          5 }t          j        |          }|                                D ]?\  }}	 t          	                    |          | j
        |<   )# t          t          f$ r Y <w xY w	 ddd           n# 1 swxY w Y   n)# t          $ r}t          d|            Y d}~nd}~ww xY wd| _         dS )	zCLoad sessions index from disk. Must be called with self._lock held.NTparentsexist_oksessions.jsonrr   encodingz,[gateway] Warning: Failed to load sessions: )r   r   mkdirexistsopenjsonloadrg   r   rM   r   r   KeyErrorr   r   )rB   sessions_filefrH   key
entry_datar   s          r   r   z"SessionStore._ensure_loaded_locked  s   < 	Ft<<<)O;!! 	J
J-w??? %19Q<<D+/::<< % %Z%1=1G1G
1S1SDM#.. *H5 % % %$H%%% % % % % % % % % % % % % % %  J J JHQHHIIIIIIIIJ s`   C .C"B)(C)B=:C<B==CC CC CC 
C?#C::C?c                    ddl }| j                            dd           | j        dz  }d | j                                        D             }|                    t          | j                  dd	          \  }}	 t          j        |d
d          5 }t          j
        ||d           |                                 t          j        |                                           ddd           n# 1 swxY w Y   t          j        ||           dS # t          $ rK 	 t          j        |           n3# t"          $ r&}t$                              d||           Y d}~nd}~ww xY w w xY w)zASave sessions index to disk (kept for session key -> ID mapping).r   NTr   r   c                 >    i | ]\  }}||                                 S r   )rG   )ra   r   entrys      r   rf   z&SessionStore._save.<locals>.<dictcomp>-  s&    MMMeU]]__MMMr   z.tmpz
.sessions_)dirsuffixr$   wr   r      )indentz!Could not remove temp file %s: %s)tempfiler   r   r   rg   mkstemprJ   osfdopenr   dumpflushfsyncfilenoreplaceBaseExceptionunlinkOSErrorr   r   )rB   r   r   rH   fdtmp_pathr   r   s           r   _savezSessionStore._save'  s   t<<<)O;MMt}7J7J7L7LMMM''D%&&vl ( 
 
H	2sW555 %	$!,,,,			$$$% % % % % % % % % % % % % % % Jx///// 	 	 	O	(#### O O O@(ANNNNNNNNO	s[   <D AC1%D 1C55D 8C59D 
E(D32E(3
E#=EE(E##E(rW   c           	      v    t          |t          | j        dd          t          | j        dd                    S )z%Generate a session key from a source.r   Tr   F)r   r   )r   getattrr   )rB   rW   s     r   _generate_session_keyz"SessionStore._generate_session_key>  sB     $+DK9RTX$Y$Y%,T[:TV[%\%\
 
 
 	
r   r   c                    | j         r|                      |j                  rdS | j                            |j        |j                  }|j        dk    rdS t                      }|j        dv r%|j        t          |j
                  z   }||k    rdS |j        dv rN|                    |j        ddd	          }|j        |j        k     r|t          d
          z  }|j        |k     rdS dS )u(  Check if a session has expired based on its reset policy.
        
        Works from the entry alone — no SessionSource needed.
        Used by the background expiry watcher to proactively flush memories.
        Sessions with active background processes are never considered expired.
        Fr,   session_typenoneidlebothminutesTdailyr   r   hourminutesecondmicrosecondr!   days)r   r[   r   get_reset_policyr,   r0   moder   r^   r   idle_minutesr   at_hourr   )rB   r   policyr   idle_deadlinetoday_resets         r   _is_session_expiredz SessionStore._is_session_expiredF  s    ( 	,,U->?? u--^ . 
 

 ;&  5ff;***!,yAT/U/U/UUM]""t;+++++^ &  K x&.((ya0000+--tur   c                    | j         r,|                     |          }|                      |          rdS | j                            |j        |j                  }|j        dk    rdS t                      }|j        dv r%|j        t          |j
                  z   }||k    rdS |j        dv rN|                    |j        ddd	          }|j        |j        k     r|t          d
          z  }|j        |k     rdS dS )a  
        Check if a session should be reset based on policy.
        
        Returns the reset reason ("idle" or "daily") if a reset is needed,
        or None if the session is still valid.
        
        Sessions with active background processes are never reset.
        Nr   r   r   r   r   r   r   r   r!   r   r   )r   r   r   r   r,   r0   r   r   r^   r   r   r   r   r   )rB   r   rW   r[   r   r   r   r  s           r   _should_resetzSessionStore._should_resetl  s,    ( 	44V<<K,,[99 t--_) . 
 

 ;&  4ff;***!,yAT/U/U/UUM]""v;+++++^	 &  K x&.((ya0000+--wtr   c                    | j         r.	 | j                                         dk    S # t          $ r Y nw xY w| j        5  |                                  t          | j                  dk    cddd           S # 1 swxY w Y   dS )u  Check if any sessions have ever been created (across all platforms).

        Uses the SQLite database as the source of truth because it preserves
        historical session records (ended sessions still count).  The in-memory
        ``_entries`` dict replaces entries on reset, so ``len(_entries)`` would
        stay at 1 for single-platform users — which is the bug this fixes.

        The current session is already in the DB by the time this is called
        (get_or_create_session runs first), so we check ``> 1``.
        r!   N)r   session_countr   r   r   lenr   ri   s    r   has_any_sessionszSessionStore.has_any_sessions  s     8 	x--//!33    Z 	* 	*&&(((t}%%)	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	* 	*s   & 
33,A66A:=A:F	force_newc                    |                      |          }t                      }d}d}| j        5  |                                  || j        v rq|so| j        |         }|j        rd}n|                     ||          }|s)||_        |                                  |cddd           S d}	|}
|j	        dk    }|j
        }nd}	d}
d}|                    d           dt          j                    j        dd          }t          ||||||j        |j        |j        |	|
|	          }|| j        |<   |                                  ||j        j        |j        d
}ddd           n# 1 swxY w Y   | j        rQ|rO	 | j                            |d           n2# t.          $ r%}t0                              d|           Y d}~nd}~ww xY w| j        r?|r=	  | j        j        di | n)# t.          $ r}t7          d|            Y d}~nd}~ww xY w|S )z
        Get an existing session or create a new one.

        Evaluates reset policy to determine if the existing session is stale.
        Creates a session record in SQLite when a new session starts.
        Nr   Tr   F%Y%m%d_%H%M%S_   )r[   r\   r]   r^   r   r   r,   r0   r   r   r   r\   rW   r1   session_resetSession DB operation failed: %sz4[gateway] Warning: Failed to create SQLite session: r   )r   r   r   r   r   r   r  r^   r   r   r\   strftimeuuiduuid4hexr   r.   r,   r0   r   r1   r   end_sessionr   r   r   create_sessionr   )rB   rW   r	  r[   r   db_end_session_iddb_create_kwargsr   reset_reasonr   r   r   r\   r   s                 r   get_or_create_sessionz"SessionStore.get_or_create_session  s    0088ff !Z 3	 3	&&(((dm++I+k2 ? E#.LL#'#5#5eV#D#DL# 
9'*E$JJLLL 3	 3	 3	 3	 3	 3	 3	 3	$ &*N(4%).);a)?&(-(8%%!&$(!%*"  LL99RRDJLL<LRaR<PRRJ '%#- *-"3#5  E */DM+&JJLLL( //!>   _3	 3	 3	 3	 3	 3	 3	 3	 3	 3	 3	 3	 3	 3	 3	l 8 	C) 	CC$$%6HHHH C C C>BBBBBBBBC 8 	R( 	RR'';;*:;;;; R R RPQPPQQQQQQQQR sI   A+E$'B1E$$E(+E(9F 
GF??GG$ $
H
.HH
r[   r   c                     | j         5  |                                  || j        v r=| j        |         }t                      |_        |||_        |                                  ddd           dS # 1 swxY w Y   dS )z9Update lightweight session metadata after an interaction.N)r   r   r   r   r^   r   r   )rB   r[   r   r   s       r   update_sessionzSessionStore.update_session  s     Z 	 	&&(((dm++k2#'66 %1/AE,

	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s   AA00A47A4c                     | j         5  |                                  || j        v r4d| j        |         _        |                                  	 ddd           dS 	 ddd           n# 1 swxY w Y   dS )zMark a session as suspended so it auto-resets on next access.

        Used by ``/stop`` to prevent stuck sessions from being resumed
        after a gateway restart (#7536).  Returns True if the session
        existed and was marked.
        TNF)r   r   r   r   r   )rB   r[   s     r   suspend_sessionzSessionStore.suspend_session  s     Z 	 	&&(((dm++7;k*4

	 	 	 	 	 	 	 	+	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 us   AA''A+.A+x   max_age_secondsc                 R   ddl m} t                       ||          z
  }d}| j        5  |                                  | j                                        D ] }|j        s|j        |k    rd|_        |dz  }!|r| 	                                 ddd           n# 1 swxY w Y   |S )a  Mark recently-active sessions as suspended.

        Called on gateway startup to prevent sessions that were likely
        in-flight when the gateway last exited from being blindly resumed
        (#7536).  Only suspends sessions updated within *max_age_seconds*
        to avoid resetting long-idle sessions that are harmless to resume.
        Returns the number of sessions that were suspended.
        r   )r   )secondsTr!   N)
r   r   r   r   r   r   valuesr   r^   r   )rB   r   r   cutoffcountr   s         r   suspend_recently_activez$SessionStore.suspend_recently_active$  s    	'&&&&&))O<<<<Z 	 	&&(((--//   5+;v+E+E&*EOQJE 

	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 s   A%BB #B c                    d}d}d}| j         5  |                                  || j        vr	 ddd           dS | j        |         }|j        }t	                      }|                    d           dt          j                    j        dd          }t          |||||j
        |j        |j        |j                  }|| j        |<   |                                  ||j        r|j        j        nd|j
        r|j
        j        ndd}ddd           n# 1 swxY w Y   | j        rQ|rO	 | j                            |d           n2# t&          $ r%}t(                              d	|           Y d}~nd}~ww xY w| j        rH|rF	  | j        j        d
i | n2# t&          $ r%}t(                              d	|           Y d}~nd}~ww xY w|S )z1Force reset a session, creating a new session ID.Nr  r  r  r[   r\   r]   r^   r   r   r,   r0   r   r  r  r  r   )r   r   r   r\   r   r  r  r  r  r   r   r   r,   r0   r   r   r1   r   r  r   r   r   r  )	rB   r[   r  r  	new_entry	old_entryr   r\   r   s	            r   reset_sessionzSessionStore.reset_session;  s    	Z 	 	&&((($-//		 	 	 	 	 	 	 	 k2I ) 4&&CLL99RRDJLL<LRaR<PRRJ$'% '&3"+#-	 	 	I *3DM+&JJLLL(6?6HW),22i7@7GQ9+33T   3	 	 	 	 	 	 	 	 	 	 	 	 	 	 	> 8 	C) 	CC$$%6HHHH C C C>BBBBBBBBC 8 	C( 	CC'';;*:;;;; C C C>BBBBBBBBC sG   DCDDD*E 
E5E00E5F 
GF??Gtarget_session_idc                    d}d}| j         5  |                                  || j        vr	 ddd           dS | j        |         }|j        |k    r|cddd           S |j        }t	                      }t          |||||j        |j        |j        |j	                  }|| j        |<   | 
                                 ddd           n# 1 swxY w Y   | j        rQ|rO	 | j                            |d           n2# t          $ r%}t                              d|           Y d}~nd}~ww xY w| j        rN	 | j                            |           n2# t          $ r%}t                              d|           Y d}~nd}~ww xY w|S )a  Switch a session key to point at an existing session ID.

        Used by ``/resume`` to restore a previously-named session.
        Ends the current session in SQLite (like reset), but instead of
        generating a fresh session ID, re-uses ``target_session_id`` so the
        old transcript is loaded on the next message. If the target session was
        previously ended, re-open it so gateway resume semantics match the CLI.
        Nr(  session_switchz!Session DB end_session failed: %sz$Session DB reopen_session failed: %s)r   r   r   r\   r   r   r   r   r,   r0   r   r   r  r   r   r   reopen_session)rB   r[   r,  r  r)  r*  r   r   s           r   switch_sessionzSessionStore.switch_sessionn  sc    !	Z 	 	&&((($-//		 	 	 	 	 	 	 	 k2I #'888 	 	 	 	 	 	 	 	 !* 4&&C$', '&3"+#-	 	 	I *3DM+&JJLLL7	 	 	 	 	 	 	 	 	 	 	 	 	 	 	: 8 	E) 	EE$$%68HIIII E E E@!DDDDDDDDE 8 	HH''(9:::: H H HCQGGGGGGGGH sM   CCACCCC9 9
D(D##D(3E 
E=E88E=active_minutesc                 H   | j         5  |                                  t          | j                                                  }ddd           n# 1 swxY w Y   |-t                      t          |          z
  fd|D             }|                    d d           |S )z3List all sessions, optionally filtered by activity.Nr   c                 *    g | ]}|j         k    |S r   r^   )ra   r   r$  s     r   rc   z.SessionStore.list_sessions.<locals>.<listcomp>  s%    DDDQQ\V-C-Cq-C-C-Cr   c                     | j         S Nr4  )r   s    r   <lambda>z,SessionStore.list_sessions.<locals>.<lambda>  s    1< r   T)r   reverse)r   r   listr   r#  r   r   sort)rB   r1  entriesr$  s      @r   list_sessionszSessionStore.list_sessions  s    Z 	3 	3&&(((4=//1122G	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 	3 %VVi????FDDDD'DDDG//>>>s   ;AAAr\   c                     | j         | dz  S )z3Get the path to a session's legacy transcript file.z.jsonl)r   )rB   r\   s     r   get_transcript_pathz SessionStore.get_transcript_path  s     j#8#8#888r   messageskip_dbc           
      X   | j         r|s	 | j                             ||                    dd          |                    d          |                    d          |                    d          |                    d                     n2# t          $ r%}t                              d|           Y d	}~nd	}~ww xY w|                     |          }t          |d
d          5 }|                    t          j
        |d          dz              d	d	d	           d	S # 1 swxY w Y   d	S )az  Append a message to a session's transcript (SQLite + legacy JSONL).

        Args:
            skip_db: When True, only write to JSONL and skip the SQLite write.
                     Used when the agent already persisted messages to SQLite
                     via its own _flush_messages_to_session_db(), preventing
                     the duplicate-write bug (#860).
        roler   content	tool_name
tool_callstool_call_id)r\   rB  rC  rD  rE  rF  r  Nar   r   Fensure_asciiro   )r   append_messagerK   r   r   r   r>  r   writer   dumps)rB   r\   r?  r@  r   transcript_pathr   s          r   append_to_transcriptz!SessionStore.append_to_transcript  s    8 	CG 	C
C'') VY77#KK	22%kk+66&{{<88!(^!<!< (      C C C>BBBBBBBBC 22:>>/3999 	DQGGDJwU;;;dBCCC	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	Ds*   B B 
B;B66B;%-DD#&D#messagesc                 L   | j         r)	 | j                             |           |D ]}|                    dd          }| j                             |||                    d          |                    d          |                    d          |                    d          |dk    r|                    d          nd	|dk    r|                    d
          nd	|dk    r|                    d          nd		  	         n2# t          $ r%}t
                              d|           Y d	}~nd	}~ww xY w|                     |          }t          |dd          5 }|D ].}|	                    t          j        |d          dz              /	 d	d	d	           d	S # 1 swxY w Y   d	S )zReplace the entire transcript for a session with new messages.
        
        Used by /retry, /undo, and /compress to persist modified conversation history.
        Rewrites both SQLite and legacy JSONL storage.
        rB  r   rC  rD  rE  rF  	assistant	reasoningNreasoning_detailscodex_reasoning_items)	r\   rB  rC  rD  rE  rF  rR  rS  rT  z&Failed to rewrite transcript in DB: %sr   r   r   FrH  ro   )r   clear_messagesrK   rJ  r   r   r   r>  r   rK  r   rL  )rB   r\   rO  msgrB  r   rM  r   s           r   rewrite_transcriptzSessionStore.rewrite_transcript  s@    8 	JJ''
333#  C776955DH++#-! #	 2 2"%''+"6"6#&77<#8#8%(WW^%<%<:>+:M:M#''+"6"6"6SWJNR]J]J]#''2E*F*F*FcgRVZeReRecgg6M.N.N.Nko , 
 
 
 
  J J JEqIIIIIIIIJ 22:>>/3999 	DQ D D
3U;;;dBCCCCD	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	D 	Ds*   C5D   
D/
D**D/2FF Fc           
         g }| j         rN	 | j                             |          }n2# t          $ r%}t                              d|           Y d}~nd}~ww xY w|                     |          }g }|                                rt          |dd          5 }|D ]z}|                                }|rb	 |	                    t          j        |                     A# t          j        $ r' t                              d||dd                    Y vw xY w{	 ddd           n# 1 swxY w Y   t          |          t          |          k    r;|r7t                              d|t          |          t          |                     |S |S )	z.Load all messages from a session's transcript.z#Could not load messages from DB: %sNr   r   r   z*Skipping corrupt line in transcript %s: %sr  uf   Session %s: JSONL has %d messages vs SQLite %d — using JSONL (legacy session not yet fully migrated))r   get_messages_as_conversationr   r   r   r>  r   r   stripr@   r   loadsJSONDecodeErrorwarningr  )rB   r\   db_messagesr   rM  jsonl_messagesr   lines           r   load_transcriptzSessionStore.load_transcript  s   8 	GG"hCCJOO G G GBAFFFFFFFFG
 22:>>!!## 	osW=== 
 	 	D::<<D *11$*T2B2BCCCC#3   "NN L *D#J    	
 
 
 
 
 
 
 
 
 
 
 
 
 
 
, ~[!1!111 JN 3 3S5E5E  
 "!sJ   & 
AAAD 1'CD 3DD DD  D$'D$r6  )r   N)F)r  )"rN   rO   rP   rQ   r   r'   r   r   r   r   r+   rJ   r   r   r   r  r	   r  r  r  r   r  r  r&  r+  r0  r   r<  r>  r   r
   rN  rW  ra  r   r   r   r   r     sB         *.f fT f= f f f f") ) ) )
   .   .
M 
c 
 
 
 
$ $$ $ $ $ $L*< * *8TW= * * * *X*$ * * * *2  U UU U 
	U U U Ut #'    
	    3 4     s S    .1 1,1G 1 1 1 1f5# 5# 5(S_J` 5 5 5 5n HSM T,EW    9c 9d 9 9 9 9D Ds DT#s(^ DVZ Dgk D D D D8DS DDc3h<P DUY D D D D@.# .$tCH~2F . . . . . .r   r   r   session_entryc                     |                                 }i }|D ]}|                    |          }|r|||<   t          | ||          }|r0|j        |_        |j        |_        |j        |_        |j        |_        |S )z
    Build a full session context from a source and config.
    
    This is used to inject context into the agent's system prompt.
    )rW   rX   rY   )get_connected_platformsget_home_channelrV   r[   r\   r]   r^   )rW   r   rb  	connectedrY   r,   r   rk   s           r   build_session_contextrg  $  s     ..00IM + +&&x00 	+&*M(#%#  G  6+7*5*5*5Nr   )TFr6  )-rQ   r   loggingr   r   r   r  pathlibr   r   r   dataclassesr   typingr   r   r	   r
   	getLoggerrN   r   r   rJ   r   r   r%   r   r&   r'   r(   r)   r+   rV   	frozensetWHATSAPPSIGNALTELEGRAMBLUEBUBBLESrp   r   r   r   r   r   rg  r   r   r   <module>rr     st      				             ( ( ( ( ( ( ( ( ! ! ! ! ! ! , , , , , , , , , , , ,		8	$	$h    BC BC B B B B
%3 %3 % % % %

 
 
 
 
 
            G
 G
 G
 G
 G
 G
 G
 G
V 
 
 
 
 
 
 
 
B  iO	!   / N N NN N 		N N N Nb h
 h
 h
 h
 h
 h
 h
 h
Z %)%*8 88!8 #8 		8 8 8 8vo o o o o o o oj -1  L) 	     r   