
    iZ                        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mZ ddlm	Z	 ddl
mZ ddlmZmZmZmZ ddlmZ dZ	 ddlZn# e$ r dZ	 ddlZn# e$ r Y nw xY wY nw xY w ej        e          Zde	fd	Zd
Zg dZh dZdedee         fdZ G d d          Z	 	 	 	 d*dededededee         defdZ de!fdZ"ddddg ddddddgdddd d!dd"d!d#ddgd$d%Z#dd&l$m%Z%m&Z&  e%j'        dde#d' e"d()           dS )+uM  
Memory Tool Module - Persistent Curated Memory

Provides bounded, file-backed memory that persists across sessions. Two stores:
  - MEMORY.md: agent's personal notes and observations (environment facts, project
    conventions, tool quirks, things learned)
  - USER.md: what the agent knows about the user (preferences, communication style,
    expectations, workflow habits)

Both are injected into the system prompt as a frozen snapshot at session start.
Mid-session writes update files on disk immediately (durable) but do NOT change
the system prompt -- this preserves the prefix cache for the entire session.
The snapshot refreshes on the next session start.

Entry delimiter: § (section sign). Entries can be multiline.
Character limits (not tokens) because char counts are model-independent.

Design:
- Single `memory` tool with action parameter: add, replace, remove, read
- replace/remove use short unique substring matching (not full text or IDs)
- Behavioral guidance lives in the tool schema description
- Frozen snapshot pattern: system prompt is stable, tool responses show live state
    N)contextmanager)Pathget_hermes_home)DictAnyListOptional)atomic_replacereturnc                  $    t                      dz  S )z-Return the profile-scoped memories directory.memoriesr        6/home/ubuntu/.hermes/hermes-agent/tools/memory_tool.pyget_memory_dirr   7   s    z))r   u   
§
))z2ignore\s+(previous|all|above|prior)\s+instructionsprompt_injection)zyou\s+are\s+now\s+role_hijack)zdo\s+not\s+tell\s+the\s+userdeception_hide)zsystem\s+prompt\s+overridesys_prompt_override)z<disregard\s+(your|all|any)\s+(instructions|rules|guidelines)disregard_rules)zVact\s+as\s+(if|though)\s+you\s+(have\s+no|don\'t\s+have)\s+(restrictions|limits|rules)bypass_restrictions)z?curl\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)
exfil_curl)z?wget\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)
exfil_wget)zAcat\s+[^\n]*(\.env|credentials|\.netrc|\.pgpass|\.npmrc|\.pypirc)read_secrets)authorized_keysssh_backdoor)z\$HOME/\.ssh|\~/\.ssh
ssh_access)z'\$HOME/\.hermes/\.env|\~/\.hermes/\.env
hermes_env>
      ​   ‌   ‍   ‪   ‫   ‬   ‭   ‮   ⁠   ﻿contentc                     t           D ]}|| v rdt          |          ddc S t          D ]-\  }}t          j        || t          j                  rd| dc S .dS )zRScan memory content for injection/exfil patterns. Returns error string if blocked.z8Blocked: content contains invisible unicode character U+04Xz (possible injection).z)Blocked: content matches threat pattern 'zn'. Memory entries are injected into the system prompt and must not contain injection or exfiltration payloads.N)_INVISIBLE_CHARSord_MEMORY_THREAT_PATTERNSresearch
IGNORECASE)r*   charpatternpids       r   _scan_memory_contentr6   \   s     ! t t7??scRViisssssss  0 s s9Wgr}55 	s ss  s  s  s  s  s  s	s 4r   c            
          e Zd ZdZd!dedefdZd Zeede	fd	                        Z
ed
ede	fd            Zd
efdZd
efdZd
edee         fdZd
edee         fdZd
edefdZd
edefdZd
ededeeef         fdZd
edededeeef         fdZd
ededeeef         fdZd
edee         fdZd"d
ededeeef         fdZd
edee         defdZede	dee         fd            Zede	dee         fd             ZdS )#MemoryStorea  
    Bounded curated memory with file persistence. One instance per AIAgent.

    Maintains two parallel states:
      - _system_prompt_snapshot: frozen at load time, used for system prompt injection.
        Never mutated mid-session. Keeps prefix cache stable.
      - memory_entries / user_entries: live state, mutated by tool calls, persisted to disk.
        Tool responses always reflect this live state.
      _  memory_char_limituser_char_limitc                 R    g | _         g | _        || _        || _        ddd| _        d S )N memoryuser)memory_entriesuser_entriesr;   r<   _system_prompt_snapshot)selfr;   r<   s      r   __init__zMemoryStore.__init__v   s8    )+')!2.BDb7Q7Q$$$r   c                     t                      }|                    dd           |                     |dz            | _        |                     |dz            | _        t          t                              | j                            | _        t          t                              | j                            | _        |                     d| j                  |                     d| j                  d| _	        dS )	zHLoad entries from MEMORY.md and USER.md, capture system prompt snapshot.Tparentsexist_ok	MEMORY.mdUSER.mdr@   rA   r?   N)
r   mkdir
_read_filerB   rC   listdictfromkeys_render_blockrD   )rE   mem_dirs     r   load_from_diskzMemoryStore.load_from_disk~   s     ""dT222"oog.CDD OOGi,?@@ #4==1D#E#EFF t/@!A!ABB ((43FGG&&vt/@AA(
 (
$$$r   pathc              #     K   |                      | j        dz             }|j                            dd           t          t
          dV  dS t
          rH|                                r|                                j        dk    r|	                    dd           t          |t
          rd	nd
          }	 t          r t	          j        |t          j                   nG|                    d           t          j        |                                t
          j        d           dV  t          r t	          j        |t          j                   ngt
          r`	 |                    d           t          j        |                                t
          j        d           n# t&          t(          f$ r Y nw xY w|                                 dS # t          r t	          j        |t          j                   ngt
          r`	 |                    d           t          j        |                                t
          j        d           n# t&          t(          f$ r Y nw xY w|                                 w xY w)zAcquire an exclusive file lock for read-modify-write safety.

        Uses a separate .lock file so the memory file itself can still be
        atomically replaced via os.replace().
        z.lockTrH   Nr    utf-8encodingzr+za+   )with_suffixsuffixparentrM   fcntlmsvcrtexistsstatst_size
write_textopenflockLOCK_EXseeklockingfilenoLK_LOCKLOCK_UNLK_UNLCKOSErrorIOErrorclose)rU   	lock_pathfds      r   
_file_lockzMemoryStore._file_lock   s:      $$T[7%:;;	td;;;=V^EEEF 	89++-- 	81A1A1IQ1N1N  w 777)V5TT66	 ?B....


ryy{{FNA>>>EEE B.... GGAJJJN299;;CCCC)   DHHJJJJJ  B.... GGAJJJN299;;CCCC)   DHHJJJJsE   9A2G AF" "F65F6/I3?AII3II3II3targetr   c                 >    t                      }| dk    r|dz  S |dz  S )NrA   rL   rK   )r   )rt   rS   s     r   	_path_forzMemoryStore._path_for   s.     ""VY&&$$r   c                     |                      |                     |                    }t          t                              |                    }|                     ||           dS )zRe-read entries from disk into in-memory state.

        Called under file lock to get the latest state before mutating.
        N)rN   rv   rO   rP   rQ   _set_entries)rE   rt   freshs      r   _reload_targetzMemoryStore._reload_target   sV    
 v 6 677T]]5))**&%(((((r   c                     t                                          dd           |                     |                     |          |                     |                     dS )zEPersist entries to the appropriate file. Called after every mutation.TrH   N)r   rM   _write_filerv   _entries_forrE   rt   s     r   save_to_diskzMemoryStore.save_to_disk   sW    td;;;//1B1B61J1JKKKKKr   c                 *    |dk    r| j         S | j        S NrA   rC   rB   r~   s     r   r}   zMemoryStore._entries_for   s    V$$""r   entriesc                 2    |dk    r	|| _         d S || _        d S r   r   rE   rt   r   s      r   rx   zMemoryStore._set_entries   s)    V 'D")Dr   c                     |                      |          }|sdS t          t                              |                    S )Nr   )r}   lenENTRY_DELIMITERjoinr   s      r   _char_countzMemoryStore._char_count   s>    ##F++ 	1?''00111r   c                 *    |dk    r| j         S | j        S r   )r<   r;   r~   s     r   _char_limitzMemoryStore._char_limit   s    V''%%r   r*   c           
      P   |                                 }|sdddS t          |          }|rd|dS |                     |                     |                    5  |                     |           |                     |          }|                     |          }||v r"|                     |d          cddd           S ||gz   }t          t          
                    |                    }||k    rH|                     |          }dd|dd|dd	t          |           d
||dd|ddcddd           S |                    |           |                     ||           |                     |           ddd           n# 1 swxY w Y   |                     |d          S )zDAppend a new entry. Returns error if it would exceed the char limit.FzContent cannot be empty.successerrorz*Entry already exists (no duplicate added).Nz
Memory at ,/z chars. Adding this entry (zI chars) would exceed the limit. Replace or remove existing entries first.)r   r   current_entriesusagezEntry added.)stripr6   rs   rv   rz   r}   r   _success_responser   r   r   r   appendrx   r   )	rE   rt   r*   
scan_errorr   limitnew_entries	new_totalcurrents	            r   addzMemoryStore.add   st   --// 	K$/IJJJ *'22
 	;$z:::__T^^F3344 	& 	&'''''//G$$V,,E '!!--f6bcc	& 	& 	& 	& 	& 	& 	& 	& "WI-KO00==>>I5  **622$EWB E EB E E.1'llE E E (/ '555E555	 	#	& 	& 	& 	& 	& 	& 	& 	&8 NN7###fg...f%%%=	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	&@ %%fn===s!   AF A.F;A FFFold_textnew_contentc                 (                                    |                                 }sdddS |sdddS t          |          }|rd|dS |                     |                     |                    5  |                     |           |                     |          }fdt          |          D             }|sdd ddcddd           S t          |          d	k    rNt          d
 |D                       }t          |          d	k    r"d |D             }dd d|dcddd           S |d         d         }	| 	                    |          }
|
                                }|||	<   t          t                              |                    }||
k    rdd|dd|
dddcddd           S |||	<   |                     ||           |                     |           ddd           n# 1 swxY w Y   |                     |d          S )zFFind entry containing old_text substring, replace it with new_content.Fold_text cannot be empty.r   z<new_content cannot be empty. Use 'remove' to delete entries.c                 &    g | ]\  }}|v 	||fS r   r   .0ier   s      r   
<listcomp>z'MemoryStore.replace.<locals>.<listcomp>  &    NNN$!QA1vr   No entry matched ''.Nr[   c              3       K   | ]	\  }}|V  
d S Nr   r   _r   s      r   	<genexpr>z&MemoryStore.replace.<locals>.<genexpr>&  &      "9"9A1"9"9"9"9"9"9r   c                 X    g | ]'\  }}|d d         t          |          dk    rdndz   (S NP   z...r>   r   r   s      r   r   z'MemoryStore.replace.<locals>.<listcomp>(  ;    \\\DAq#2#3q66B;;%%B G\\\r   Multiple entries matched ''. Be more specific.r   r   matchesr   z Replacement would put memory at r   r   z> chars. Shorten the new content or remove other entries first.zEntry replaced.)r   r6   rs   rv   rz   r}   	enumerater   setr   copyr   r   rx   r   r   )rE   rt   r   r   r   r   r   unique_textspreviewsidxr   test_entriesr   s     `          r   replacezMemoryStore.replace  s,   >>##!'')) 	L$/JKKK 	o$/mnnn *+66
 	;$z:::__T^^F3344 (	& (	&'''''//GNNNN)G*<*<NNNG V#(3T3T3T3TUU(	& (	& (	& (	& (	& (	& (	& (	& 7||a""9"9"9"9"999|$$q((\\T[\\\H#(!\h!\!\!\#+ (	& (	& (	& (	& (	& (	& (	& (	&* !*Q-C$$V,,E #<<>>L +LO00>>??I5  $R9Z R RZ R R R =(	& (	& (	& (	& (	& (	& (	& (	&L 'GCLfg...f%%%Q(	& (	& (	& (	& (	& (	& (	& (	& (	& (	& (	& (	& (	& (	& (	&T %%f.?@@@s'   6AG3AG34A6G370G33G7:G7c                                                     sdddS |                     |                     |                    5  |                     |           |                     |          }fdt          |          D             }|sdd ddcddd           S t          |          dk    rNt          d	 |D                       }t          |          dk    r"d
 |D             }dd d|dcddd           S |d         d         }|                    |           | 	                    ||           | 
                    |           ddd           n# 1 swxY w Y   |                     |d          S )z/Remove the entry containing old_text substring.Fr   r   c                 &    g | ]\  }}|v 	||fS r   r   r   s      r   r   z&MemoryStore.remove.<locals>.<listcomp>Q  r   r   r   r   Nr[   c              3       K   | ]	\  }}|V  
d S r   r   r   s      r   r   z%MemoryStore.remove.<locals>.<genexpr>X  r   r   c                 X    g | ]'\  }}|d d         t          |          dk    rdndz   (S r   r   r   s      r   r   z&MemoryStore.remove.<locals>.<listcomp>Z  r   r   r   r   r   r   zEntry removed.)r   rs   rv   rz   r}   r   r   r   poprx   r   r   )rE   rt   r   r   r   r   r   r   s     `     r   removezMemoryStore.removeG  s-   >>## 	L$/JKKK__T^^F3344 	& 	&'''''//GNNNN)G*<*<NNNG V#(3T3T3T3TUU	& 	& 	& 	& 	& 	& 	& 	& 7||a""9"9"9"9"999|$$q((\\T[\\\H#(!\h!\!\!\#+ 	& 	& 	& 	& 	& 	& 	& 	&* !*Q-CKKfg...f%%%1	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	&4 %%f.>???s!   AE"AEAEE!$E!c                 D    | j                             |d          }|r|ndS )at  
        Return the frozen snapshot for system prompt injection.

        This returns the state captured at load_from_disk() time, NOT the live
        state. Mid-session writes do not affect this. This keeps the system
        prompt stable across all turns, preserving the prefix cache.

        Returns None if the snapshot is empty (no entries at load time).
        r>   N)rD   get)rE   rt   blocks      r   format_for_system_promptz$MemoryStore.format_for_system_prompti  s+     ,00<<'uu4'r   Nmessagec           	      (   |                      |          }|                     |          }|                     |          }|dk    r#t          dt	          ||z  dz                      nd}d||| d|dd|ddt          |          d}|r||d	<   |S )
Nr   d   T   % — r   r   z chars)r   rt   r   r   entry_countr   )r}   r   r   minintr   )rE   rt   r   r   r   r   pctresps           r   r   zMemoryStore._success_responsex  s    ##F++""6**  ((8=		c#sGeOs233444q >>7>>>u>>>>w<<
 
  	&%DOr   c                 @   |sdS |                      |          }t                              |          }t          |          }|dk    r#t	          dt          ||z  dz                      nd}|dk    rd| d|dd|dd	}nd
| d|dd|dd	}d}| d| d| d| S )z=Render a system prompt block with header and usage indicator.r>   r   r   rA   z USER PROFILE (who the user is) [r   r   r   z chars]zMEMORY (your personal notes) [u   ══════════════════════════════════════════════
)r   r   r   r   r   r   )	rE   rt   r   r   r*   r   r   header	separators	            r   rR   zMemoryStore._render_block  s     	2  ((!&&w//g,,8=		c#sGeOs233444qV___7___u____FF]c]]]]]U]]]]F	??v????g???r   c                    |                                  sg S 	 |                     d          }n# t          t          f$ r g cY S w xY w|                                sg S d |                    t                    D             }d |D             S )zRead a memory file and split into entries.

        No file locking needed: _write_file uses atomic rename, so readers
        always see either the previous complete file or the new complete file.
        rX   rY   c                 6    g | ]}|                                 S r   )r   r   r   s     r   r   z*MemoryStore._read_file.<locals>.<listcomp>  s     AAA17799AAAr   c                     g | ]}||S r   r   r   s     r   r   z*MemoryStore._read_file.<locals>.<listcomp>  s    (((aa((((r   )ra   	read_textrn   ro   r   splitr   )rU   rawr   s      r   rN   zMemoryStore._read_file  s     {{}} 	I	..'.22CC! 	 	 	III	 yy{{ 	I BAcii&@&@AAA((7((((s   / AAc                    |rt                               |          nd}	 t          j        t	          | j                  dd          \  }}	 t          j        |dd          5 }|                    |           |	                                 t          j
        |                                           ddd           n# 1 swxY w Y   t          ||            dS # t          $ r( 	 t          j        |           n# t          $ r Y nw xY w w xY w# t          t           f$ r}t#          d	|  d
|           d}~ww xY w)aq  Write entries to a memory file using atomic temp-file + rename.

        Previous implementation used open("w") + flock, but "w" truncates the
        file *before* the lock is acquired, creating a race window where
        concurrent readers see an empty file. Atomic rename avoids this:
        readers always see either the old complete file or the new one.
        r>   z.tmpz.mem_)dirr]   prefixwrX   rY   NzFailed to write memory file z: )r   r   tempfilemkstempstrr^   osfdopenwriteflushfsyncrj   r   BaseExceptionunlinkrn   ro   RuntimeError)rU   r   r*   rr   tmp_pathfr   s          r   r|   zMemoryStore._write_file  s    4;B/&&w///	K#+$$VG  LBYr3999 )QGGG$$$GGIIIHQXXZZ((() ) ) ) ) ) ) ) ) ) ) ) ) ) ) x.....    Ih''''   D ! 	K 	K 	KIdIIaIIJJJ	Ksr   ,D C $AC 4C  CC CC 
D(C=<D=
D
D	D

DD D=#D88D=)r9   r:   r   )__name__
__module____qualname____doc__r   rF   rT   staticmethodr   r   rs   r   rv   rz   r   r	   r}   rx   r   r   r   r   r   r   r   r
   r   r   rR   rN   r|   r   r   r   r8   r8   k   s/        R R# Rs R R R R
 
 
$ ! ! ! ! ^ \!F %# %$ % % % \%)S ) ) ) )L3 L L L L
#3 #49 # # # #
*3 *c * * * *2# 2# 2 2 2 2&# &# & & & &
+># +> +>S#X +> +> +> +>Z8Ac 8AS 8As 8AtCQTH~ 8A 8A 8A 8At @S  @C  @DcN  @  @  @  @D(s (x} ( ( ( (  c T#s(^    "@C @$s) @ @ @ @ @$ ) )$s) ) ) ) \)* K$ Kc K K K \K K Kr   r8   r@   actionrt   r   storec                    |t          dd          S |dvrt          d| dd          S | dk    r*|st          d	d          S |                    ||          }n| d
k    r>|st          dd          S |st          dd          S |                    |||          }nE| dk    r*|st          dd          S |                    ||          }nt          d|  dd          S t	          j        |d          S )z{
    Single entry point for the memory tool. Dispatches to MemoryStore methods.

    Returns JSON string with results.
    NzJMemory is not available. It may be disabled in config or this environment.F)r   r?   zInvalid target 'z'. Use 'memory' or 'user'.r   z%Content is required for 'add' action.r   z*old_text is required for 'replace' action.z)content is required for 'replace' action.r   z)old_text is required for 'remove' action.zUnknown action 'z'. Use: add, replace, remove)ensure_ascii)
tool_errorr   r   r   jsondumps)r   rt   r*   r   r   results         r   memory_toolr     sX    }fpuvvvv'''OVOOOY^____ 	VEuUUUU67++	9		 	[JTYZZZZ 	ZISXYYYYvx99	8		 	ZISXYYYYfh// QVQQQ[`aaaa:f51111r   c                      dS )z=Memory tool has no external requirements -- always available.Tr   r   r   r   check_memory_requirementsr     s    4r   a  Save durable information to persistent memory that survives across sessions. Memory is injected into future turns, so keep it compact and focused on facts that will still matter later.

WHEN TO SAVE (do this proactively, don't wait to be asked):
- User corrects you or says 'remember this' / 'don't do that again'
- User shares a preference, habit, or personal detail (name, role, timezone, coding style)
- You discover something about the environment (OS, installed tools, project structure)
- You learn a convention, API quirk, or workflow specific to this user's setup
- You identify a stable fact that will be useful again in future sessions

PRIORITY: User preferences and corrections > environment facts > procedural knowledge. The most valuable memory prevents the user from having to repeat themselves.

Do NOT save task progress, session outcomes, completed-work logs, or temporary TODO state to memory; use session_search to recall those from past transcripts.
If you've discovered a new way to do something, solved a problem that could be necessary later, save it as a skill with the skill tool.

TWO TARGETS:
- 'user': who the user is -- name, role, preferences, communication style, pet peeves
- 'memory': your notes -- environment facts, project conventions, tool quirks, lessons learned

ACTIONS: add (new entry), replace (update existing -- old_text identifies it), remove (delete -- old_text identifies it).

SKIP: trivial/obvious info, things easily re-discovered, raw data dumps, and temporary task state.objectstring)r   r   r   zThe action to perform.)typeenumdescriptionrA   zIWhich memory store: 'memory' for personal notes, 'user' for user profile.z4The entry content. Required for 'add' and 'replace'.)r  r  zBShort unique substring identifying the entry to replace or remove.)r   rt   r*   r   )r  
propertiesrequired)namer  
parameters)registryr   c           	          t          |                     dd          |                     dd          |                     d          |                     d          |                    d                    S )	Nr   r>   rt   r@   r*   r   r   )r   rt   r*   r   r   )r   r   )argskws     r   <lambda>r  >  sb    {xx"%%xx(++##*%%ffWoo      r   u   🧠)r  toolsetschemahandlercheck_fnemoji)r@   NNN)(r   r   loggingr   r0   r   
contextlibr   pathlibr   hermes_constantsr   typingr   r   r	   r
   utilsr   r`   r_   ImportError	getLoggerr   loggerr   r   r/   r-   r   r6   r8   r   boolr   MEMORY_SCHEMAtools.registryr
  r   registerr   r   r   <module>r!     s;   0   				 				  % % % % % %       , , , , , , , , , , , , , , , , , ,             
LLLL   E   	 
	8	$	$* * * * *    &   # (3-    cK cK cK cK cK cK cK cKP #'&2 &2&2&2 &2 	&2
 K &2 	&2 &2 &2 &2R4     	m.  !4447  !!6*j  !U 
 !c 
 
( x(- 31 1j 0 / / / / / / /  	  '
     s5   A AAAAAAAA