
    o;i)              	       v   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 ddlm Z m!Z! ddl"m#Z# e G d d                      Z$e G d d                      Z% e&ej'        ej(        ej)        ej*        h          Z+	 de,fdZ-ddde%de,defdZ.e G d d                      Z/dddde$d e,d!e,de,fd"Z0	 	 d)de$d e,d!e,defd#Z1 G d$ d%          Z2	 d*de$d&ed'ee/         de%fd(Z3d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     4/home/ubuntu/.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HomeChannel)canonical_whatsapp_identifiernormalize_whatsapp_identifier)atomic_replacec                      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<   dZ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_altFis_botguild_idparent_chat_id
message_idr   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 terminalr2   DM with usergroupgroup: channel	channel: zthread: , )r/   r&   LOCALr3   appendr5   r4   r1   r0   r6   join)selfpartss     r   descriptionzSessionSource.description_   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<   | j        r
| j        |d<   | j        r
| j        |d<   | j        r
| j        |d<   |S )N)r/   r0   r1   r3   r4   r5   r6   r7   r8   r9   r;   r<   r=   )r/   r   r0   r1   r3   r4   r5   r6   r7   r8   r9   r;   r<   r=   )rI   ds     r   to_dictzSessionSource.to_dictt   s    +||/	
 	
  	0#/Am 	0#/Am= 	* MAjM 	6"&"5A? 	."oAlOr   datac                 "    | t          |d                   t          |d                   |                    d          |                    dd          |                    d          |                    d          |                    d          |                    d	          |                    d
          |                    d          |                    d          |                    d          |                    d                    S )Nr/   r0   r1   r3   r2   r4   r5   r6   r7   r8   r9   r;   r<   r=   )r/   r0   r1   r3   r4   r5   r6   r7   r8   r9   r;   r<   r=   )r&   strget)clsrO   s     r   	from_dictzSessionSource.from_dict   s    sd:.//Y((hh{++hh{D11HHY''hh{++hh{++xx--////XXj))88$455xx--
 
 
 	
r   )__name__
__module____qualname____doc__r&   __annotations__rQ   r1   r	   r3   r4   r5   r6   r7   r8   r9   r:   boolr;   r<   r=   propertyrK   r   r
   rN   classmethodrT   r   r   r   r.   r.   F   s          LLL#Ix}###Is!GXc]!!!#Ix}####Ix}### $J$$$!%K#%%%!%K#%%%FD"Hhsm"""$(NHSM((( $J$$$ 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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_channelsFshared_multi_user_session session_key
session_idN
created_at
updated_atr   c           	      D   | j                                         d | j        D             d | j                                        D             | j        | 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   rN   )rj   rk   hcs      r   
<dictcomp>z*SessionContext.to_dict.<locals>.<dictcomp>   s5       */!R  r   )r_   r`   ra   rb   rd   re   rf   rg   )r_   rN   r`   ra   itemsrb   rd   re   rf   	isoformatrg   rI   s    r   rN   zSessionContext.to_dict   s    k))++#N#NT5M#N#N#N 373E3K3K3M3M   *.)G+/9=R$/33555d9=R$/33555d
 
 	
r   )rU   rV   rW   rX   r.   rY   r   r&   r   r)   rb   rZ   rd   rQ   re   rf   r	   r   rg   r
   rN   r   r   r   r^   r^      s           h'''+-....&+t+++ KJ%)J")))%)J")))
c3h 
 
 
 
 
 
r   r^   c                      t           j                            d          pd                                sdS 	 ddlm}  ddlm}  |             } ||dd          }d|v pd	|v S # t          $ r Y dS w xY w)
uD  True iff the agent will actually have Discord tools this session.

    Two conditions must hold:
      1. The `discord` or `discord_admin` toolset is enabled for the
         Discord platform via `hermes tools` (opt-in, default OFF).
      2. `DISCORD_BOT_TOKEN` is set — the tool's `check_fn` gates on it
         at registry time, so the toolset being enabled in config is not
         enough if the token isn't configured.

    Returns False (safe default — keeps the stale-API disclaimer) on any
    error so a bad config can't silently promise tools the agent lacks.
    DISCORD_BOT_TOKENrc   Fr   )load_config)_get_platform_toolsdiscord)include_default_mcp_serversdiscord_admin)	osenvironrR   striphermes_cli.configru   hermes_cli.tools_configrv   	Exception)ru   rv   cfgenableds       r   _discord_tools_loadedr      s     JNN.//52<<>> u111111??????kmm%%c9RWXXXG#A''AA   uus   +A# #
A10A1F)
redact_piicontextr   c          
         | j         j        t          v }|sG	 ddlm} |                    | j         j        j                  }|r	|j        rd}n# t          $ r Y nw xY w|o|}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        r*| j         j        rdnd}|                    d| 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           n| j         j        t          j        k    rt5                      r| j         }ddg}|j        r|                    d|j         d           |j        rD|j        r=|                    d|j         d           |                    d|j         d           n|                    d|j         d           |j        r|                    d |j         d           |                    |           n|                    d           |                    d!           n| 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           |                    d0 |             d1           | j#        $                                D ]+\  }}|                    d2|j         d3|j%         d           ,|                    d           |                    d4           d5"                    |          S )6ay  
    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).
    r   )platform_registryTz## Current Session Contextrc   z**Source:** z! (the machine running this agent)r@   r2   r?   rA   rB   rC   rD   z ()z**Channel Topic:** zMulti-user threadzMulti-user sessionz**Session type:** uN    — messages are prefixed with [sender name]. Multiple users may participate.z
**User:** z**User ID:** un  **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. The gateway may inline the current message's Slack block/attachment payload when available, but you still cannot call Slack APIs yourself.z<**Discord IDs (for the `discord` / `discord_admin` tools):**z  - Guild: ``z  - Parent channel: `z  - Thread: `z/` (use as `channel_id` for fetch_messages etc.)z  - Channel: `z  - Triggering message: `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.u  **Platform notes:** You are responding via iMessage. Keep responses short and conversational — think texts, not essays. Structure longer replies as separate short thoughts, each separated by a blank line (double newline). Each block between blank lines will be delivered as its own iMessage bubble, so write accordingly: one idea per bubble, 1–3 sentences each. If the user needs a detailed answer, give the short version first and offer to elaborate.z**Platform notes:** You are running inside Yuanbao. You CAN send private (DM) messages via the send_message tool. Use target='yuanbao:direct:<account_id>' for DM and target='yuanbao:group:<group_code>' for group chat.zlocal (files on this machine)u   : Connected ✓z**Connected Platforms:** rE   z)**Home Channels (default destinations):**z  - z: z (ID: z)**Delivery options for scheduled tasks:**)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.*
)(r_   r/   _PII_SAFE_PLATFORMSgateway.platform_registryr   rR   r   pii_safer   titler&   rF   rG   r5   r4   r   r1   r%   r0   r3   rK   r7   rb   r6   SLACKDISCORDr   r;   r<   r=   extendBLUEBUBBLESYUANBAOr`   rH   ra   rp   namehermes_constantsr   )r   r   _is_pii_safer   entrylinesplatform_namesrc_uname_cnamedescsession_labeluidid_linesplatforms_listrk   r/   homehc_idr   _origin_labels                        r   build_session_context_promptr      sj   * >*.AAL 	CCCCCC%))'.*A*GHHE $ $# 	 	 	D	,J$
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 ( ,/6~/Ga++MaB B B B	
 	
 	
 	
 
	! ,<'.":<<====		 ,n$ 	'!#&&C*S**+++ ~(.00R9	
 	
 	
 	
 
	 H$4	4	4 !"" 	.CZ[H| @ >s| > > >???} A!3 A M8J M M MNNN n n n noooo ? ? ? ?@@@~ O MCN M M MNNNLL""""LLLLT    
	 H$8	8	8R&		
 		
 		
 		
 
	 H$4	4	4RF	
 	
 	
 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s   5A 
AA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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$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.
    rd   re   rf   rg   Norigindisplay_namer/   r2   r3   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is_fresh_resetexpiry_finalized	suspendedresume_pendingresume_reasonlast_resume_marked_atr   c                 0   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        | j        | j        r| j                                        nd | j        d}| j        r| j                                        |d<   |S )Nrd   re   rf   rg   r   r/   r3   r   r   r   r   r   r   r   r   r   r   )r   r   r   r   r   )rd   re   rf   rq   rg   r   r/   r   r3   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   rN   )rI   results     r   rN   zSessionEntry.to_dict  s   
4+
$/
 $/3355
 $/3355	

 D-
 t}F++$
 
 D-
 T/
  !7
 !$"9
 D-
 !$"9
 !$"9
 4+
   5!
" #
$ #1!/ -*44666"13
 
 
6 ; 	5#{2244F8r   rO   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 }|                    d          }|r/	 t          j        |          }n# t          t          f$ r d }Y n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d          d|                    dd          d|                    d          d|d|                    dd          S )Nr   r/   zUnknown platform value %r: %sr   rd   re   rf   rg   r   r3   r2   r   r   r   r   r   r   r   r   r   r   r   r   memory_flushedFr   r   r   r   r   )
r.   rT   rR   r&   
ValueErrorloggerdebugr   fromisoformat	TypeError)rS   rO   r   r/   er   _lrmas          r   rT   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 !%011 	--(0(>u(E(E%%z* - - -(,%%%- 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
  	:::!
" "XX&8$((CSUZ:[:[\\\#
$ hh{E222%
&  88$4e<<<'
( ((?333)
* #8"7+
,  88$4e<<<-
 	
s*   A 
B'"BB0C CC)'rU   rV   rW   rX   rQ   rY   r   r   r	   r.   r   r/   r&   r3   r   intr   r   r   r   r   floatr   r   r   rZ   r   r   r   r   r   r   r   r   r   r
   rN   r\   rT   r   r   r   r   r     s.         
 OOO '+FH]#*** #'L(3-&&&#'Hhx '''Is L#M3sL# #### K      !ND   '+x}+++$$$$ !ND    #d"""
 It !ND   #'M8C='''048H-444c3h    @ +
T#s(^ +
 +
 +
 +
 [+
 +
 +
r   r   Tgroup_sessions_per_userthread_sessions_per_userr_   r   r   c                6    | j         dk    rdS | j        r| S | S )an  Return True when a non-DM session is shared across participants.

    Mirrors the isolation rules in :func:`build_session_key`:
      - DMs are never shared.
      - Threads are shared unless ``thread_sessions_per_user`` is True.
      - Non-thread group/channel sessions are shared unless
        ``group_sessions_per_user`` is True (default: True = isolated).
    r2   F)r3   r6   )r_   r   r   s      r   is_shared_multi_user_sessionr   =  s4     4u ,+++&&&r   c                    | j         j        }| j        dk    rk| j        }| j         t          j        k    rt          | j                  }|r| j        rd| d| d| j         S d| d| S | j        rd| d| j         S d| dS | j        p| j	        }|r3| j         t          j        k    rt          t          |                    p|}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.
    r2   zagent:main:z:dm:r    z:dmz
agent:mainF)r/   r   r3   r0   r&   WHATSAPPr*   r6   r8   r4   rQ   rG   rH   )r_   r   r   r/   
dm_chat_idparticipant_id	key_partsisolate_users           r   build_session_keyr   R  s   8 $H4^
?h///6v~FFJ 	< SRXRR:RR@PRRR;;;z;;; 	BAAAv/?AAA*X****'96>N ^&/X->>> 7s>7J7JKK]~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	 d0dededefdZdedefdZdedefdZd1d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 )2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   r   print)rI   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_lockedrr   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loadrp   r   rT   r   r   KeyErrorr   r   )rI   sessions_filefrO   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          ||           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   )rN   )rj   r   r   s      r   ro   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   rp   mkstemprQ   rz   fdopenr   dumpflushfsyncfilenor,   BaseExceptionunlinkOSErrorr   r   )rI   r  r   rO   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 %	$!,,,,			$$$% % % % % % % % % % % % % % % 8]33333 	 	 	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#D.-E#.
E8EE#EE#r_   c           	      v    t          |t          | j        dd          t          | j        dd                    S )z%Generate a session key from a source.r   Tr   Fr   )r   getattrr   )rI   r_   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   rd   r   get_reset_policyr/   r3   moder   rg   r   idle_minutesreplaceat_hourr  )rI   r   policyr   idle_deadlinetoday_resets         r   _is_session_expiredz SessionStore._is_session_expired  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/   r3   r&  r   rg   r   r'  r(  r)  r  )rI   r   r_   rd   r*  r   r+  r,  s           r   _should_resetzSessionStore._should_reset  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   rr   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 r|s| j        |         }|j        rd}nF|j        r)||_        |                                  |cddd           S | 	                    ||          }|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# t0          $ r%}t2                              d|           Y d}~nd}~ww xY w| j        r?|r=	  | j        j        di | n)# t0          $ r}t9          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_   )rd   re   rf   rg   r   r   r/   r3   r   r   r   re   r_   r4   session_resetSession DB operation failed: %sz4[gateway] Warning: Failed to create SQLite session: r   )r  r   r   r   r   r   r   rg   r  r/  r   re   strftimeuuiduuid4hexr   r1   r/   r3   r   r4   r   end_sessionr   r   r   create_sessionr   )rI   r_   r4  rd   r   db_end_session_iddb_create_kwargsr   reset_reasonr   r   r   re   r   s                 r   get_or_create_sessionz"SessionStore.get_or_create_sessionR  s    0088ff !Z A	 A	&&(((dm++I+k2 ? E#.LL) E (+E$JJLLL /A	 A	 A	 A	 A	 A	 A	 A	2 $(#5#5eV#D#DL# 
9'*E$JJLLL ;A	 A	 A	 A	 A	 A	 A	 A	@ &*N(4%).);a)?&(-(8%%!&$(!%*"  LL99RRDJLL<LRaR<PRRJ '%#- *-"3#5  E */DM+&JJLLL( //!>   {A	 A	 A	 A	 A	 A	 A	 A	 A	 A	 A	 A	 A	 A	 A	H 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 sO   AF4FB1FFF)G 
G4G//G4H 
H:H55H:rd   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   rg   r   r  )rI   rd   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  )rI   rd   s     r   suspend_sessionzSessionStore.suspend_session  s     Z 	 	&&(((dm++7;k*4

	 	 	 	 	 	 	 	+	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 us   AA''A+.A+restart_timeoutreasonc                 J   | j         5  |                                  || j        v re| j        |         }|j        r	 ddd           dS d|_        ||_        t                      |_        |                                  	 ddd           dS 	 ddd           n# 1 swxY w Y   dS )a  Mark a session as resumable after a restart interruption.

        Unlike ``suspend_session()``, this preserves the existing
        ``session_id`` and the transcript.  The next call to
        ``get_or_create_session()`` for this key returns the same entry
        so the user auto-resumes on the same conversation lane.

        Returns True if the session existed and was marked.
        NFT)	r   r   r   r   r   r   r   r   r  )rI   rd   rK  r   s       r   mark_resume_pendingz SessionStore.mark_resume_pending  s#    Z 	 	&&(((dm++k2 ? ! 	 	 	 	 	 	 	 	 (,$&,#.2ff+

	 	 	 	 	 	 	 	+	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 us   3B6BBBc                 $   | j         5  |                                  | j                            |          }||j        s	 ddd           dS d|_        d|_        d|_        |                                  	 ddd           dS # 1 swxY w Y   dS )a/  Clear the resume-pending flag after a successful resumed turn.

        Called from the gateway after ``run_conversation()`` returns a
        final response for a session that had ``resume_pending=True``,
        signalling that recovery succeeded.

        Returns True if a flag was cleared.
        NFT)r   r   r   rR   r   r   r   r  )rI   rd   r   s      r   clear_resume_pendingz!SessionStore.clear_resume_pending  s     Z 		 		&&(((M%%k22E}E$8}			 		 		 		 		 		 		 		
 $)E "&E*.E'JJLLL		 		 		 		 		 		 		 		 		 		 		 		 		 		 		 		 		 		s   9B*BB	B	max_age_daysc                    ||dk    rdS ddl m} t                       ||          z
  }g }| j        5  |                                  t          | j                                                  D ]\  }}|j        r| j	        U	 | 	                    |j
                  r0n8# t          $ r+}t                              d|j
        |           Y d}~nd}~ww xY w|j        |k     r|                    |           |D ]}| j                            |d           |r|                                  ddd           n# 1 swxY w Y   |r)t                              dt'          |          |           t'          |          S )u=  Drop SessionEntry records older than max_age_days.

        Pruning is based on ``updated_at`` (last activity), not ``created_at``.
        A session that's been active within the window is kept regardless of
        how old it is.  Entries marked ``suspended`` are kept — the user
        explicitly paused them for later resume.  Entries held by an active
        process (via has_active_processes_fn) are also kept so long-running
        background work isn't orphaned.

        Pruning is functionally identical to a natural reset-policy expiry:
        the transcript in SQLite stays, but the session_key → session_id
        mapping is dropped and the user starts a fresh session on return.

        ``max_age_days <= 0`` disables pruning; returns 0 immediately.
        Returns the number of entries removed.
        Nr   r   r#  z6has_active_processes_fn raised during prune for %s: %sz1SessionStore pruned %d entries older than %d days)r   r   r   r   r   listr   rp   r   r   rd   r   r   r   rg   rG   popr  infor2  )rI   rP  r   cutoffremoved_keysr   r   excs           r   prune_old_entrieszSessionStore.prune_old_entries  s   " <1#4#41&&&&&&))6666"$Z 	 	&&((("4=#6#6#8#899 - -
U?  0<889JKK %$%$   T!-s       
 #f,, '',,,# - -!!#t,,,, 

1	 	 	 	 	 	 	 	 	 	 	 	 	 	 	4  	KKCL!!<   <   s=   AD<B!D<!
C+!CD<CAD<<E E x   max_age_secondsc                    ddl m} t                       ||          z
  }d}| j        5  |                                  | j                                        D ]B}|j        r
|j        s1|j	        |k    r&d|_        d|_
        t                      |_        |dz  }C|r|                                  ddd           n# 1 swxY w Y   |S )a  Mark recently-active sessions as resumable after an unexpected exit.

        Called on gateway startup after a crash or fast restart to preserve
        in-flight sessions instead of destroying their conversation history
        (#7536).  Only marks sessions updated within *max_age_seconds* to
        avoid touching long-idle sessions.  Sets ``resume_pending=True`` so
        the next incoming message on the same session_key auto-resumes from
        the existing transcript.

        Entries already flagged ``resume_pending=True`` are skipped.  Entries
        explicitly ``suspended=True`` (from /stop or stuck-loop escalation)
        are also skipped.  Terminal escalation for genuinely stuck sessions
        is still handled by the existing ``.restart_failure_counts`` counter
        (threshold 3), which runs after this method and sets ``suspended=True``.

        Returns the number of sessions marked resumable.
        r   rR  )secondsTrestart_interruptedr!   N)r   r   r   r   r   r   valuesr   r   rg   r   r   r  )rI   r[  r   rV  countr   s         r   suspend_recently_activez$SessionStore.suspend_recently_active@  s)   $ 	'&&&&&))O<<<<Z 	 	&&(((--//  '  5+;v+E+E+/E(*?E'26&&E/QJE 

	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 s   BB>>CC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        d	  	        }|| 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.Nr6  r7  r8  T)	rd   re   rf   rg   r   r   r/   r3   r   r   r9  r:  r;  r   )r   r   r   re   r   r<  r=  r>  r?  r   r   r   r/   r3   r  r   r4   r   r@  r   r   r   rA  )	rI   rd   rB  rC  	new_entry	old_entryr   re   r   s	            r   reset_sessionzSessionStore.reset_sessiond  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   5	 	 	 	 	 	 	 	 	 	 	 	 	 	 	@ 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 
E6E11E6F 
G G  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.
        N)rd   re   rf   rg   r   r   r/   r3   session_switchz!Session DB end_session failed: %sz$Session DB reopen_session failed: %s)r   r   r   re   r   r   r   r   r/   r3   r  r   r@  r   r   r   reopen_session)rI   rd   rf  rB  rc  rd  r   r   s           r   switch_sessionzSessionStore.switch_session  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   rg   )rj   r   rV  s     r   rl   z.SessionStore.list_sessions.<locals>.<listcomp>  s%    DDDQQ\V-C-Cq-C-C-Cr   c                     | j         S Nrn  )r   s    r   <lambda>z,SessionStore.list_sessions.<locals>.<lambda>  s    1< r   T)r   reverse)r   r   rS  r   r_  r   r   sort)rI   rk  entriesrV  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re   c                     | j         | dz  S )z3Get the path to a session's legacy transcript file.z.jsonl)r   )rI   re   s     r   get_transcript_pathz SessionStore.get_transcript_path  s     j#8#8#888r   messageskip_dbc                 2   | j         r|s	 | j                             ||                    dd          |                    d          |                    d          |                    d          |                    d          |                    d          dk    r|                    d          nd	|                    d          dk    r|                    d
          nd	|                    d          dk    r|                    d          nd	|                    d          dk    r|                    d          nd	|                    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 }|                    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	assistant	reasoningNreasoning_contentreasoning_detailscodex_reasoning_itemscodex_message_items)re   r{  r|  r}  r~  r  r  r  r  r  r  r;  ar   r   Fensure_asciir   )r   append_messagerR   r   r   r   rw  r   writer   dumps)rI   re   rx  ry  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!(^!<!<:A++f:M:MQ\:\:\gkk+666bfJQ++V\J]J]alJlJlgkk2E&F&F&FrvJQ++V\J]J]alJlJlgkk2E&F&F&FrvRYR]R]^dReReitRtRt'++6M*N*N*Nz~NUkkZ`NaNaepNpNp4I(J(J(Jvz (      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*   E+E9 9
F(F##F(-HHHmessagesc                    | j         rO	 | j                             ||           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.
        z&Failed to rewrite transcript in DB: %sNr   r   r   Fr  r   )
r   replace_messagesr   r   r   rw  r   r  r   r  )rI   re   r  r   r  r   msgs          r   rewrite_transcriptzSessionStore.rewrite_transcript  sU    8 	JJ))*h???? 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&   % 
AAA>2B>>CC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: %srZ  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   rw  r   r   r|   rG   r   loadsJSONDecodeErrorwarningr2  )rI   re   db_messagesr   r  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$rp  )r   N)F)rJ  )rZ  )%rU   rV   rW   rX   r   r'   r   r   r   r  r.   rQ   r  r   rZ   r-  r	   r/  r3  rE  r   rG  rI  rM  rO  rY  ra  re  rj  r   ru  rw  r   r
   r  r  r  r   r   r   r   r     s         *.f fT f= f f f f") ) ) )
   .   .
M 
c 
 
 
 
$ $$ $ $ $ $L*< * *8TW= * * * *X*$ * * * *2  c cc c 
	c c c cP #'    
	    3 4    $ (   
	   :     (7!c 7!c 7! 7! 7! 7!r" "s "S " " " "H2 2,1G 2 2 2 2h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BDS DDc3h<P DUY D D D D(.# .$tCH~2F . . . . . .r   r   r   session_entryc                 Z   |                                 }i }|D ]}|                    |          }|r|||<   t          | ||t          | t	          |dd          t	          |dd                              }|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.
    r   Tr   Fr   )r_   r`   ra   rb   )	get_connected_platformsget_home_channelr^   r   r  rd   re   rf   rg   )r_   r   r  	connectedra   r/   r   r   s           r   build_session_contextr  G  s     ..00IM + +&&x00 	+&*M(#%#">$+F4Mt$T$T%,V5OQV%W%W#
 #
 #
		 	 	G  6+7*5*5*5Nr   )TFrp  )4rX   r   loggingrz   r   r   r=  pathlibr   r   r   dataclassesr   typingr   r   r	   r
   	getLoggerrU   r   r   rQ   r   r   r%   r   r&   r'   r(   r)   whatsapp_identityr*   r+   utilsr,   r.   r^   	frozensetr   SIGNALTELEGRAMr   r   rZ   r   r   r   r   r   r   r  r   r   r   <module>r     s       				             ( ( ( ( ( ( ( ( ! ! ! ! ! ! , , , , , , , , , , , ,		8	$	$h    BC BC B B B B
%3 %3 % % % %

 
 
 
 
 
                   !           T
 T
 T
 T
 T
 T
 T
 T
p  
  
  
  
  
  
  
  
F  iO	!   /
t    8 ~ ~ ~~ ~ 		~ ~ ~ ~B Q
 Q
 Q
 Q
 Q
 Q
 Q
 Q
n %)%*	' ' '' "' #	'
 
' ' ' '. %)%*A AA!A #A 		A A A AHn
 n
 n
 n
 n
 n
 n
 n
h -1# ### L)# 	# # # # # #r   