
    )j                        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ddlmZ dedee         fdZdddedeeef         fdZ  G d d          Z!dededee         dee         dee         f
dZ"	 	 	 	 d0dededededee!         defdZ#de$fdZ%deeef         dddeeef         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 )1uM  
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   
§
)first_threat_messagecontentc                 $    t          | d          S )zRScan memory content for injection/exfil patterns. Returns error string if blocked.strictscope)_first_threat_message)r   s    r   _scan_memory_contentr   N   s     9999r   pathr   bak_pathc                 (    dd| j          d| d|ddS )u  Build the error dict returned when external drift is detected.

    The on-disk memory file contains content that wouldn't round-trip
    through the tool's parser/serializer — flushing would discard the
    appended/edited content from a patch tool, shell append, manual edit,
    or sister-session write. We refuse the mutation, point the operator at
    the .bak.<ts> snapshot we took, and tell them what to do next.
    FzRefusing to write z: file on disk has content that wouldn't round-trip through the memory tool (likely added by the patch tool, a shell append, a manual edit, or a concurrent session). A snapshot was saved to u   . Resolve the drift first — either rewrite the file as a clean §-delimited list of entries, or move the extra content out — then retry. This guard exists to prevent silent data loss (issue #26045).zOpen the .bak file, integrate the missing entries into the memory tool one at a time via memory(action=add, content=...), then remove or rewrite the original file to a clean state.)successerrordrift_backupremediation)name)r   r   s     r   _drift_errorr#   S   sI        =E   !I  r   c            
       x   e Zd ZdZd$dedefdZd Zedee	         d	e	d
ee	         fd            Z
eedefd                        Zede	d
efd            Zde	d
e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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__|   s8    )+')!2.BDb7Q7Q$$$r   c                 X   t                      }|                    dd           |                     |dz            | _        |                     |dz            | _        t          t                              | j                            | _        t          t                              | j                            | _        |                     | j        d          }|                     | j        d          }| 	                    d|          | 	                    d|          d| _
        dS )	uy  Load entries from MEMORY.md and USER.md, capture system prompt snapshot.

        The frozen snapshot is what enters the system prompt. We scan each
        entry for injection/promptware patterns at snapshot-build time —
        ANY hit replaces the entry text in the snapshot with a placeholder
        like ``[BLOCKED: …]``, so a poisoned-on-disk memory file (supply
        chain, compromised tool, sister-session write) cannot inject into
        the system prompt.

        The live ``memory_entries`` / ``user_entries`` lists keep the
        original text so the user can still SEE poisoned entries via
        ``memory(action=read)`` and remove them — silently dropping them
        would hide the attack from the user.

        Scanning is deterministic from disk bytes, so the snapshot remains
        stable for the entire session (prefix-cache invariant holds).
        Tparentsexist_ok	MEMORY.mdUSER.mdr-   r.   r,   N)r   mkdir
_read_filer/   r0   listdictfromkeys_sanitize_entries_for_snapshot_render_blockr1   )r2   mem_dirsanitized_memorysanitized_users       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
  >>t?RT_``<<T=NPYZZ ((3CDD&&v~>>(
 (
$$$r   entriesfilenamer   c           	         ddl m} g }| D ]}|r|                    d          r|                    |           / ||d          }|r_t                              d|d                    |                     |                    d| d	d                    |           d
           |                    |           |S )uK  Return ``entries`` with any threat-matching entry replaced by a placeholder.

        Each entry is scanned with the shared threat-pattern library at the
        ``"strict"`` scope (same as memory writes).  On match, the entry is
        replaced in the returned list with ``"[BLOCKED: <filename> entry
        contained threat pattern: <ids>. Removed from system prompt.]"`` —
        the placeholder enters the snapshot, the original entry stays in
        live state for the user to inspect and delete.

        Empty or already-block-marker entries pass through unchanged.
        r   )scan_for_threatsz	[BLOCKED:r   r   z-Memory entry from %s blocked at load time: %sz, z
[BLOCKED: z$ entry contained threat pattern(s): zs. Removed from system prompt; use memory(action=read) to inspect and memory(action=remove) to delete the original.])tools.threat_patternsrH   
startswithappendloggerwarningjoin)rE   rF   rH   	sanitizedentryfindingss         r   r?   z*MemoryStore._sanitize_entries_for_snapshot   s    	;:::::!	 	( 	(E E,,[99   '''''X>>>H (Cdii11     0 0 0yy**0 0 0      ''''r   r   c              #     K   |                      | j        dz             }|j                            dd           t          t
          dV  dS t          |dd          }	 t          r t	          j        |t          j                   nG|	                    d           t          j
        |                                t
          j        d	           dV  t          r8	 t	          j        |t          j                   n~# t          t          f$ r Y nkw xY wt
          r`	 |	                    d           t          j
        |                                t
          j        d	           n# t          t          f$ r Y nw xY w|                                 dS # t          r8	 t	          j        |t          j                   n~# t          t          f$ r Y nkw xY wt
          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.lockTr5   Nza+utf-8encodingr      )with_suffixsuffixparentr:   fcntlmsvcrtopenflockLOCK_EXseeklockingfilenoLK_LOCKLOCK_UNOSErrorIOErrorLK_UNLCKclose)r   	lock_pathfds      r   
_file_lockzMemoryStore._file_lock   s?      $$T[7%:;;	td;;;=V^EEEF)TG444	 ?B....


ryy{{FNA>>>EEE 
KEM2222)   D GGAJJJN299;;CCCC)   DHHJJJJJ  
KEM2222)   D GGAJJJN299;;CCCC)   DHHJJJJs{   #A2F C= =DDAE$ $E87E8IF:9I:GIG
IAH! I!H52I4H55Itargetc                 >    t                      }| dk    r|dz  S |dz  S )Nr.   r9   r8   )r   )rk   rA   s     r   	_path_forzMemoryStore._path_for   s.     ""VY&&$$r   c                     |                      |          }|                     |          }|                     |          }t          t                              |                    }|                     ||           |S )u  Re-read entries from disk into in-memory state.

        Called under file lock to get the latest state before mutating.
        Returns the backup path if external drift was detected (the on-disk
        file contains content that wouldn't round-trip through our
        parser/serializer, OR an entry larger than the store's char limit).
        When drift is detected the caller must abort the mutation —
        flushing would discard the un-roundtrippable content.
        Returns None on clean reload.
        )rm   _detect_external_driftr;   r<   r=   r>   _set_entries)r2   rk   r   bakfreshs        r   _reload_targetzMemoryStore._reload_target   sm     ~~f%%))&11%%T]]5))**&%(((
r   c                     t                                          dd           |                     |                     |          |                     |                     dS )zEPersist entries to the appropriate file. Called after every mutation.Tr5   N)r   r:   _write_filerm   _entries_forr2   rk   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 Nr.   r0   r/   rw   s     r   rv   zMemoryStore._entries_for  s    V$$""r   c                 2    |dk    r	|| _         d S || _        d S rz   r{   r2   rk   rE   s      r   rp   zMemoryStore._set_entries  s)    V 'D")Dr   c                     |                      |          }|sdS t          t                              |                    S )Nr   )rv   lenENTRY_DELIMITERrN   r}   s      r   _char_countzMemoryStore._char_count  s>    ##F++ 	1?''00111r   c                 *    |dk    r| j         S | j        S rz   )r)   r(   rw   s     r   _char_limitzMemoryStore._char_limit$  s    V''%%r   r   c           
         |                                 }|sdddS t          |          }|rd|dS |                     |                     |                    5  |                     |          }|r/t          |                     |          |          cddd           S |                     |          }|                     |          }||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.r   r   Nz*Entry already exists (no duplicate added).z
Memory at ,/z chars. Adding this entry (u    chars) would exceed the limit. Consolidate now: use 'replace' to merge overlapping entries into shorter ones or 'remove' stale or less important entries (see current_entries below), then retry this add — all in this turn.r   r   current_entriesusagezEntry added.)stripr   rj   rm   rs   r#   rv   r   _success_responser   r   rN   r   rK   rp   rx   )
r2   rk   r   
scan_errorrq   rE   limitnew_entries	new_totalcurrents
             r   addzMemoryStore.add)  s   --// 	K$/IJJJ *'22
 	;$z:::__T^^F3344 %	& %	&
 %%f--C A#DNN6$:$:C@@%	& %	& %	& %	& %	& %	& %	& %	& ''//G$$V,,E '!!--f6bcc%	& %	& %	& %	& %	& %	& %	& %	&" "WI-KO00==>>I5  **622$]WB ] ]B ] ].1'll] ] ] (/ '555E555 -%	& %	& %	& %	& %	& %	& %	& %	&F NN7###fg...f%%%K%	& %	& %	& %	& %	& %	& %	& %	& %	& %	& %	& %	& %	& %	& %	&N %%fn===s'   :F8!AF81A.F8,A F88F<?F<old_textnew_contentc                                                     |                                 }sdddS |sdddS t          |          }|rd|dS |                     |                     |                    5  |                     |          }|r/t          |                     |          |          cddd           S |                     |          }fdt          |          D             }|sdd ddcddd           S t          |          d	k    rAd
 |D             }t          |          d	k    r"d |D             }	dd d|	dcddd           S |d         d         }
| 	                    |          }|
                                }|||
<   t          t                              |                    }||k    r8|                     |          }dd|d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.Nc                 &    g | ]\  }}|v 	||fS r   r   .0ier   s      r   
<listcomp>z'MemoryStore.replace.<locals>.<listcomp>q  &    NNN$!QA1vr   No entry matched ''.rV   c                     h | ]\  }}|S r   r   r   _r   s      r   	<setcomp>z&MemoryStore.replace.<locals>.<setcomp>x      666da666r   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>z  ;    \\\DAq#2#3q66B;;%%B G\\\r   Multiple entries matched ''. Be more specific.r   r   matchesr   z Replacement would put memory at r   r   u    chars. Shorten the new content, or 'remove' other stale or less important entries to make room (see current_entries below), then retry — all in this turn.r   zEntry replaced.)r   r   rj   rm   rs   r#   rv   	enumerater   r   copyr   rN   r   rp   rx   r   )r2   rk   r   r   r   rq   rE   r   unique_textspreviewsidxr   test_entriesr   r   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 /	& /	&%%f--C A#DNN6$:$:C@@/	& /	& /	& /	& /	& /	& /	& /	&
 ''//GNNNN)G*<*<NNNG V#(3T3T3T3TUU/	& /	& /	& /	& /	& /	& /	& /	& 7||a66g666|$$q((\\T[\\\H#(!\h!\!\!\#+ !/	& /	& /	& /	& /	& /	& /	& /	&. !*Q-C$$V,,E #<<>>L +LO00>>??I5  **622$)9Z ) )Z ) ) )
 (/ '555E555
 
C/	& /	& /	& /	& /	& /	& /	& /	&Z 'GCLfg...f%%%_/	& /	& /	& /	& /	& /	& /	& /	& /	& /	& /	& /	& /	& /	& /	&b %%f.?@@@s,   6:H5=:H5AH5BH590H55H9<H9c                 D                                    sdddS |                     |                     |                    5  |                     |          }|r/t	          |                     |          |          cddd           S |                     |          }fdt          |          D             }|sdd ddcddd           S t          |          dk    rAd	 |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   Nc                 &    g | ]\  }}|v 	||fS r   r   r   s      r   r   z&MemoryStore.remove.<locals>.<listcomp>  r   r   r   r   rV   c                     h | ]\  }}|S r   r   r   s      r   r   z%MemoryStore.remove.<locals>.<setcomp>  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>  r   r   r   r   r   r   zEntry removed.)r   rj   rm   rs   r#   rv   r   r   poprp   rx   r   )	r2   rk   r   rq   rE   r   r   r   r   s	     `      r   removezMemoryStore.remove  so   >>## 	L$/JKKK__T^^F3344 	& 	&%%f--C A#DNN6$:$:C@@	& 	& 	& 	& 	& 	& 	& 	&
 ''//GNNNN)G*<*<NNNG V#(3T3T3T3TUU	& 	& 	& 	& 	& 	& 	& 	& 7||a66g666|$$q((\\T[\\\H#(!\h!\!\!\#+ !	& 	& 	& 	& 	& 	& 	& 	&. !*Q-CKKfg...f%%%5	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	&8 %%f.>???s&   :F:FAF'AFFF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)r1   get)r2   rk   blocks      r   format_for_system_promptz$MemoryStore.format_for_system_prompt  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   rk   rE   r   entry_countr   )rv   r   r   minintr   )r2   rk   r   rE   r   r   pctresps           r   r   zMemoryStore._success_response  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   r.   z USER PROFILE (who the user is) [r   r   r   z chars]zMEMORY (your personal notes) [u   ══════════════════════════════════════════════
)r   r   rN   r   r   r   )	r2   rk   rE   r   r   r   r   header	separators	            r   r@   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.
        rS   rT   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   )exists	read_textrd   re   r   splitr   )r   rawrE   s      r   r;   zMemoryStore._read_file  s     {{}} 	I	..'.22CC! 	 	 	III	 yy{{ 	I BAcii&@&@AAA((7((((s   / AAc                 &   |                      |          }|                                sdS 	 |                    d          }n# t          t          f$ r Y dS w xY w|                                sdS d |                    t                    D             }t                              |          }| 	                    |          }t          d |D             d          }|                                |k    p||k    }|sdS t          t          j                              }	|                    |j        d|	 z             }
	 |
                    |d           n)# t          t          f$ r t!          |
          d	z   cY S w xY wt!          |
          S )
u  Return a backup-path string if on-disk content shows external drift.

        The memory file is supposed to be a list of small entries the tool
        wrote, joined by §. Detect drift via two signals:

        1. Round-trip mismatch — re-parsing and re-serializing the file
           doesn't produce identical bytes (rare; would catch oddly-encoded
           delimiters).
        2. Entry-size overflow — any single parsed entry exceeds the
           store's whole-file char limit. The tool budgets the ENTIRE store
           against that limit; no single tool-written entry can exceed it.
           When we see one entry larger than the limit, an external writer
           (patch tool, shell append, manual edit, sister session) appended
           free-form content into what the tool will treat as one entry.
           Flushing would then truncate that entry to the model's new
           content, discarding the appended bytes — issue #26045.

        Returns the absolute path of the .bak file when drift was found and
        backed up; returns None when the file looks tool-shaped.

        Note: this is an INSTANCE method (not static) because we need the
        per-target char_limit for signal #2.
        NrS   rT   c                 ^    g | ]*}|                                 |                                 +S r   r   r   s     r   r   z6MemoryStore._detect_external_drift.<locals>.<listcomp>,  s-    MMM17799M!''))MMMr   c              3   4   K   | ]}t          |          V  d S Nr   r   s     r   	<genexpr>z5MemoryStore._detect_external_drift.<locals>.<genexpr>0  s(      44SVV444444r   r   )defaultz.bak.u+    (BACKUP FAILED — file unchanged on disk))rm   r   r   rd   re   r   r   r   rN   r   maxr   timerW   rX   
write_textstr)r2   rk   r   r   parsed	roundtrip
char_limitmax_entry_lendrift_detectedtsr   s              r   ro   z"MemoryStore._detect_external_drift
  s   0 ~~f%%{{}} 	4	..'.22CC! 	 	 	44	yy{{ 	4MMSYY%?%?MMM#((00	%%f--
44V444a@@@))++2S
8R 	4
 ##DK,",,$>??	Qg6666! 	Q 	Q 	Qx==#PPPPP	Q8}}s#   A AAE #F F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_)dirrX   prefixwrS   rT   NzFailed to write memory file : )r   rN   tempfilemkstempr   rY   osfdopenwriteflushfsyncra   r   BaseExceptionunlinkrd   re   RuntimeError)r   rE   r   ri   tmp_pathfr   s          r   ru   zMemoryStore._write_fileA  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=)r&   r'   r   )!__name__
__module____qualname____doc__r   r3   rD   staticmethodr	   r   r?   r   r   rj   rm   r
   rs   rx   rv   rp   r   r   r   r   r   r   r   r   r   r@   r;   ro   ru   r   r   r   r%   r%   q   s        R R# Rs R R R R&
 &
 &
P !S	 !S !TRUY ! ! ! \!F ! ! ! ! ^ \!F %# %$ % % % \%S Xc]    $L3 L L L L
#3 #49 # # # #
*3 *c * * * *2# 2# 2 2 2 2&# &# & & & &
2># 2> 2>S#X 2> 2> 2> 2>h?Ac ?AS ?As ?AtCQTH~ ?A ?A ?A ?AB"@S "@C "@DcN "@ "@ "@ "@H(s (x} ( ( ( (  c T#s(^    "@C @$s) @ @ @ @ @$ ) )$s) ) ) ) \)*5S 5Xc] 5 5 5 5n K$ Kc K K K \K K Kr   r%   actionrk   r   c           	         | dvrdS 	 ddl m} n# t          $ r Y dS w xY w|dk    rdnd}| dk    r
d	| }|pd
}n| dk    rd| }d| d| }n	d| }|pd
}|                    |j        ||          }|j        rdS |j        rt          |j        d          S | |||d}	|	                    |j        |	| d|dd          |
                                          }
t          j        dd|
d         |j        dd          S )a  Evaluate the memory write gate. Returns a JSON tool-result string when
    the write should NOT proceed normally (blocked or staged), or None when the
    caller should perform the real write.

    Only the mutating actions (add/replace/remove) are gated.
    >   r   r   r   Nr   )write_approvalr.   zuser profiler-   r   zadd to r+   r   zreplace in zold: z
new: zremove from )inline_summaryinline_detailFr   r   rk   r   r   r   x   )summaryoriginTid)r   staged
pending_idr   ensure_ascii)toolsr   	Exceptionevaluate_gateMEMORYallowblocked
tool_errorr   stage_writecurrent_originjsondumps)r   rk   r   r   walabelr  detaildecisionpayloadrecords              r   _apply_write_gater  a  s    111t.......    tt %..NNHE#E##B	9		'''333'33(((R	'QWXXH~ t ;(*E:::: 	 G ^^
	7,,fTcTl,,  ""   F
 :Dt$	& 	&   s    
r-   storec                 8   |t          dd          S |dvrt          d| dd          S | dk    r|st          d	d          S | d
k    r|r|s|sdnd}t          | dd          S | dk    r|st          dd          S t          | |||          }||S | dk    r|                    ||          }nP| d
k    r|                    |||          }n2| dk    r|                    ||          }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.Fr   >   r.   r-   zInvalid target 'z'. Use 'memory' or 'user'.r   z%Content is required for 'add' action.r   r   r   z" is required for 'replace' action.r   z)old_text is required for 'remove' action.zUnknown action 'z'. Use: add, replace, remover  )r  r  r   r   r   r  r  )r   rk   r   r   r  missinggate_resultresults           r   memory_toolr!    s    }fpuvvvv'''OVOOOY^____ wA5QQQQHG$,;**)WHHHRWXXXX(EuUUUU $FFGXFFK67++	9		vx99	8		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   r  c                 p   |                      d          }|                      dd          }|                      d          pd}|                      d          pd}|dk    r|                    ||          S |dk    r|                    |||          S |d	k    r|                    ||          S d
d| ddS )zReplay a staged memory write directly against the store, bypassing the
    write gate. Called by the /memory approve handler.

    Returns the store's result dict.
    r   rk   r-   r   r+   r   r   r   r   FzUnknown staged action 'r   r   )r   r   r   r   )r  r  r   rk   r   r   s         r   apply_memory_pendingr%    s     [[""F[[8,,Fkk)$$*G{{:&&,"Hyy)))}}VXw777||FH---'K'K'K'KLLLr   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descriptionr.   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  )r(  
propertiesrequired)r"   r*  
parameters)registryr  c           	          t          |                     dd          |                     dd          |                     d          |                     d          |                    d                    S )	Nr   r+   rk   r-   r   r   r  )r   rk   r   r   r  )r!  r   )argskws     r   <lambda>r2    sb    {xx"%%xx(++##*%%ffWoo      r   u   🧠)r"   toolsetschemahandlercheck_fnemoji)r-   NNN),r   r  loggingr   r   r   
contextlibr   pathlibr   hermes_constantsr   typingr   r   r	   r
   utilsr   r[   rZ   ImportError	getLoggerr   rL   r   r   rI   r   r   r   r   r#   r%   r  r!  boolr#  r%  MEMORY_SCHEMAtools.registryr.  r  registerr   r   r   <module>rD     s   0   				   % % % % % %       , , , , , , , , , , , , , , , , , ,             
LLLL   E   	 
	8	$	$* * * * *   P O O O O O:# :(3- : : : :
v  c3h    <mK mK mK mK mK mK mK mK`6c 63 6# 6 (62:3-6 6 6 6v #'.2 .2.2.2 .2 	.2
 K .2 	.2 .2 .2 .2b4    
M$sCx. M M4PSUXPX> M M M M* 	m.  !4447  !!6*j  !U 
 !c 
 
( x(- 31 1j 0 / / / / / / /  	  '
     s5   A AAAAAAAA