
    (Gj                     R   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;dZ"dededee         dee         dee         f
dZ#dedeeeef                  dee         fdZ$dddededefdZ%	 	 	 	 	 	 d<dededededeeeeef                           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#d$g d%d/d$d0d+d$d1d+d2dgd3d4d5dgd3d6Z*dd7l+m,Z,m-Z-  e,j.        dde*d8 e(d9:           dS )=uG  
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
- 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        7/home/rurouni/.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            
       `   e Zd ZdZdZd1dedefdZd2d
Zdee	e
f         dee	e
f         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dde	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e	e
f                  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!ed3dee	         d*edee	         fd+            Z"d4d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d0            Z'd	S )5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                 `    g | _         g | _        || _        || _        ddd| _        d| _        d S )N memoryuserr   )memory_entriesuser_entriesr)   r*   _system_prompt_snapshot_consolidation_failures)selfr)   r*   s      r   __init__zMemoryStore.__init__   sB    )+')!2.BDb7Q7Q$ ()$$$r   r   Nc                     d| _         dS )zFReset the per-turn consolidation-failure counter (call at turn start).r   N)r3   )r4   s    r   reset_consolidation_failuresz(MemoryStore.reset_consolidation_failures   s    '($$$r   responsec                 d    | xj         dz  c_         | j         | j        k    r|S ddd| j          ddS )u  Count an at-capacity consolidation failure and degrade gracefully.

        Under the per-turn cap, return ``response`` unchanged (it already tells
        the model how to self-correct + retry in this turn). Once the cap is
        exceeded, drop the retry instruction and return a TERMINAL result so the
        model stops looping memory calls and proceeds to answer the user — a
        failed memory side effect must never block the turn's reply (#42405).
           FTzMemory consolidation failed u    times this turn. Stop retrying memory calls — leave memory unchanged for now and continue with your reply to the user. The fact can be saved in a later turn.)r   doner   )r3   $_MAX_CONSOLIDATION_FAILURES_PER_TURN)r4   r8   s     r   _consolidation_failurez"MemoryStore._consolidation_failure   s\     	$$)$$'4+TTTO#t/K # # #		
 	
 		
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 )	u  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
        see poisoned entries by inspecting the source files directly, 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_filer0   r1   listdictfromkeys_sanitize_entries_for_snapshot_render_blockr2   )r4   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filenamec           	         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): zP. Removed from system prompt; use memory(action=remove) to delete the original.])tools.threat_patternsrR   
startswithappendloggerwarningjoin)rO   rP   rR   	sanitizedentryfindingss         r   rI   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.lockTr?   Nza+utf-8encodingr   r:   )with_suffixsuffixparentrD   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/   rC   rB   )r   )rt   rK   s     r   	_path_forzMemoryStore._path_for  s.     ""VY&&$$r   F
skip_driftrx   c                   |                      |          }|rdn|                     |          }|                     |          }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.

        When *skip_drift* is True the round-trip / entry-size check is
        bypassed.  Used by the ``add`` action which appends without
        rewriting, so existing content is never clobbered.
        N)rv   _detect_external_driftrE   rF   rG   rH   _set_entries)r4   rt   rx   r   bakfreshs         r   _reload_targetzMemoryStore._reload_target  sv     ~~f%% Iddd&A&A&&I&I%%T]]5))**&%(((
r   c                     t                                          dd           |                     |                     |          |                     |                     dS )zEPersist entries to the appropriate file. Called after every mutation.Tr?   N)r   rD   _write_filerv   _entries_forr4   rt   s     r   save_to_diskzMemoryStore.save_to_disk5  sW    td;;;//1B1B61J1JKKKKKr   c                 *    |dk    r| j         S | j        S Nr/   r1   r0   r   s     r   r   zMemoryStore._entries_for:  s    V$$""r   c                 2    |dk    r	|| _         d S || _        d S r   r   r4   rt   rO   s      r   r{   zMemoryStore._set_entries?  s)    V 'D")Dr   c                     |                      |          }|sdS t          t                              |                    S )Nr   )r   lenENTRY_DELIMITERrX   r   s      r   _char_countzMemoryStore._char_countE  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_limitK  s    V''%%r   r   c                 z   |                                 }|sdddS t          |          }|rd|dS |                     |                     |                    5  |                     |d           |                     |          }|                     |          }||v r"|                     |d          cddd           S ||gz   }t          t          
                    |                    }||k    r[|                     |          }|                     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   Trw   z*Entry already exists (no duplicate added).Nz
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   rs   rv   r~   r   r   _success_responser   r   rX   r   r=   rU   r{   r   )	r4   rt   r   
scan_errorrO   limitnew_entries	new_totalcurrents	            r   addzMemoryStore.addP  s   --// 	K$/IJJJ *'22
 	;$z:::__T^^F3344 %	& %	& 4888''//G$$V,,E '!!--f6bcc%	& %	& %	& %	& %	& %	& %	& %	&" "WI-KO00==>>I5  **62222$]WB ] ]B ] ].1'll] ] ] (/ '555E5554 4  -%	& %	& %	& %	& %	& %	& %	& %	&F NN7###fg...f%%%K%	& %	& %	& %	& %	& %	& %	& %	& %	& %	& %	& %	& %	& %	& %	&N %%fn===s!   AFB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  |                     |          }|r/t          |                     |          |          cddd           S |                     |          }fdt          |          D             }|s)|                     dd d|d	          cddd           S t          |          d
k    rTd |D             }t          |          d
k    r5| 
                    d |D                       }	dd d|	dcddd           S |d         d         }
|                     |          }|                                }|||
<   t          t                              |                    }||k    rK|                     |          }|                     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>  &    NNN$!QA1vr   No entry matched 'z^'. Check current_entries below and retry with the exact text of the entry you want to replace.r   r   r   r:   c                     h | ]\  }}|S r   r   r   _r   s      r   	<setcomp>z&MemoryStore.replace.<locals>.<setcomp>      666da666r   c                     g | ]\  }}|S r   r   r   s      r   r   z'MemoryStore.replace.<locals>.<listcomp>      .E.E.ETQq.E.E.Er   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   rs   rv   r~   r#   r   	enumerater=   r   	_previewsr   copyr   rX   r   r{   r   r   )r4   rt   r   r   r   r|   rO   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 3	& 3	&%%f--C A#DNN6$:$:C@@3	& 3	& 3	& 3	& 3	& 3	& 3	& 3	&
 ''//GNNNN)G*<*<NNNG 22$ [(  [  [  ['.4 4  3	& 3	& 3	& 3	& 3	& 3	& 3	& 3	& 7||a66g666|$$q((#~~.E.EW.E.E.EFFH#(!\h!\!\!\#+ )3	& 3	& 3	& 3	& 3	& 3	& 3	& 3	&6 !*Q-C$$V,,E #<<>>L +LO00>>??I5  **62222$)9Z ) )Z ) ) )
 (/ '555E555
4 
4 
 
K3	& 3	& 3	& 3	& 3	& 3	& 3	& 3	&b 'GCLfg...f%%%g3	& 3	& 3	& 3	& 3	& 3	& 3	& 3	& 3	& 3	& 3	& 3	& 3	& 3	& 3	&j %%f.?@@@s-   6:I/=AI/AI/?B'I/30I//I36I3c                                                     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    rTd
 |D             }t          |          d	k    r5| 	                    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   z]'. Check current_entries below and retry with the exact text of the entry you want to remove.r   r:   c                     h | ]\  }}|S r   r   r   s      r   r   z%MemoryStore.remove.<locals>.<setcomp>  r   r   c                     g | ]\  }}|S r   r   r   s      r   r   z&MemoryStore.remove.<locals>.<listcomp>  r   r   r   r   r   r   zEntry removed.)r   rs   rv   r~   r#   r   r   r=   r   r   popr{   r   r   )	r4   rt   r   r|   rO   r   r   r   r   s	     `      r   removezMemoryStore.remove  s   >>## 	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 22$ Z(  Z  Z  Z'.4 4  	& 	& 	& 	& 	& 	& 	& 	& 7||a66g666|$$q((#~~.E.EW.E.E.EFFH#(!\h!\!\!\#+ )	& 	& 	& 	& 	& 	& 	& 	&6 !*Q-CKKfg...f%%%=	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	&@ %%f.>???s'   :F(AF('AF(AF((F,/F,
operationsc                 	   |sdddS t          |          D ]Z\  }}|pi                     d          }|pi                     d          }|dv r#|r!t          |          }|rdd|dz    d	| dc S [|                     |                     |                    5  |                     |          }|r/t          |                     |          |          cd
d
d
           S t          |                     |                    | 	                    |          }	t          |          D ]\  }}|pi }|                    d          }|                    d          pd
                                }
|                    d          pd
                                d|dz    d|pd d}|dk    rD|
s'|                     || d          c cd
d
d
           S |
v r                    |
           |dk    rs'|                     || d          c cd
d
d
           S |
s'|                     || d          c cd
d
d
           S fdt                    D             }|s*|                     || d d          c cd
d
d
           S t          fd|D                       dk    r*|                     || d d          c cd
d
d
           S |
|d         <   |dk    r։s'|                     || d          c cd
d
d
           S fdt                    D             }|s*|                     || d d          c cd
d
d
           S t          fd|D                       dk    r*|                     || d d          c cd
d
d
           S                     |d                    |                     || d          c cd
d
d
           S r't          t                                                  nd}||	k    rn|                     |          }|                     dd t          |           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&t          |           d'          S )(a  Apply a sequence of add/replace/remove ops to one target atomically.

        All operations are validated and applied against the FINAL budget --
        intermediate overflow is irrelevant. This lets the model free space
        (remove/replace) and add new entries in a SINGLE tool call instead of
        the multi-turn consolidate-then-retry dance that re-sends the whole
        conversation context several times.

        Semantics: all-or-nothing. If any op is malformed, doesn't match, or
        the net result would exceed the char limit, NOTHING is written and an
        error is returned describing the first failure plus the live state.
        Fzoperations list is empty.r   actionr   >   r   r   z
Operation r:   : Nr,   r   z (unknown)r   z: content is required.r   z: old_text is required.z6: content is required (use action='remove' to delete).c                 "    g | ]\  }}|v 	|S r   r   r   jr   r   s      r   r   z+MemoryStore.apply_batch.<locals>.<listcomp>*  "    QQQTQ8q==q===r   z: no entry matched ''.c                      h | ]
}|         S r   r   r   r   workings     r   r   z*MemoryStore.apply_batch.<locals>.<setcomp>-      8881GAJ888r   z: 'z8' matched multiple distinct entries -- be more specific.r   r   c                 "    g | ]\  }}|v 	|S r   r   r   s      r   r   z+MemoryStore.apply_batch.<locals>.<listcomp>7  r   r   c                      h | ]
}|         S r   r   r   s     r   r   z*MemoryStore.apply_batch.<locals>.<setcomp>:  r   r   z.: unknown action. Use add, replace, or remove.zAfter applying all z  operations, memory would be at r   r   zs chars -- over the limit. Remove or shorten more entries in the same batch (see current_entries below), then retry.r   zApplied z operation(s).)r   getr   rs   rv   r~   r#   rF   r   r   r   _batch_errorrU   r   r   r   rX   r   r=   r{   r   r   )r4   rt   r   r   opactr   r   r|   r   r   posr   r   r   r   r   s                  @@r   apply_batchzMemoryStore.apply_batch  sF     	L$/JKKK z** 	[ 	[EAr8..**C8..33K((([(1+>>
 [',7YAE7Y7YZ7Y7YZZZZZ__T^^F3344 M	& M	&%%f--C A#DNN6$:$:C@@M	& M	& M	& M	& M	& M	& M	& M	& "&d&7&7&?&?!@!@G$$V,,E":.. 1 12X2ffX&&66),,299;;FF:..4";;==?1q5??C,<9???%<<" Y#00C9W9W9WXXXX%M	& M	& M	& M	& M	& M	& M	& M	&& ')) NN7++++I%%# Z#00C9X9X9XYYYY3M	& M	& M	& M	& M	& M	& M	& M	&4 # #00""ZZZ     7M	& M	& M	& M	& M	& M	& M	& M	&> RQQQYw-?-?QQQG" c#00C9a9aU]9a9a9abbbbCM	& M	& M	& M	& M	& M	& M	& M	&D 888888899A==#00""iixiii     GM	& M	& M	& M	& M	& M	& M	& M	&N +2GGAJ''H__# Z#00C9X9X9XYYYYWM	& M	& M	& M	& M	& M	& M	& M	&X RQQQYw-?-?QQQG" c#00C9a9aU]9a9a9abbbb]M	& M	& M	& M	& M	& M	& M	& M	&^ 888888899A==#00""iixiii     aM	& M	& M	& M	& M	& M	& M	& M	&h KK
++++  ,,NNN   oM	& M	& M	& M	& M	& M	& M	& M	&z ?FLO0099:::1I5  **62222$^c*oo ^ ^$c^ ^).c^ ^ ^ (,'8'8'@'@ '555E555	4 	4 	 	AM	& M	& M	& M	& M	& M	& M	& M	&X fg...f%%%[M	& M	& M	& M	& M	& M	& M	& M	& M	& M	& M	& M	& M	& M	& M	&^ %%f.XZ.X.X.XYYYsV   :S #C'S =S !S 
:S <S /S :S <S &7S *BS 	+S  SSmessagec           	          |                      |          }|                     |          }|                     d|dz   |                     |          |dd|dd          S )z@Build a batch-abort error that reports live (uncommitted) state.Fz6 No operations were applied (batch is all-or-nothing).r   r   r   )r   r   r=   r   )r4   rt   r   r   r   s        r   r   zMemoryStore._batch_error\  s~    ""6**  ((**WW#0088---E---	,
 ,
   	r   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)r2   r   )r4   rt   blocks      r   format_for_system_promptz$MemoryStore.format_for_system_promptg  s+     ,00<<'uu4'r   P   widthc                      fd| D             S )z:Truncated one-line previews of entries for error feedback.c                 T    g | ]$}|d          t          |          k    rdndz   %S )Nz...r,   r   )r   r   r   s     r   r   z)MemoryStore._previews.<locals>.<listcomp>y  s8    OOO!&5&	c!ffunnUU"=OOOr   r   )rO   r   s    `r   r   zMemoryStore._previewsv  s      POOOwOOOOr   c           	      @   d| _         |                     |          }|                     |          }|                     |          }|dk    r#t	          dt          ||z  dz                      nd}dd|| d|dd|ddt          |          d}|r||d	<   d
|d<   |S )Nr   d   T   % — r   r   z chars)r   r;   rt   r   entry_countr   u:   Write saved. This update is complete — do not repeat it.note)r3   r   r   r   minintr   )r4   rt   r   rO   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SV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   rX   r   r   r   )	r4   rt   rO   r   r   r   r   header	separators	            r   rJ   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.
        r]   r^   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_textrm   rn   r   splitr   )r   rawrO   s      r   rE   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.
        Nr]   r^   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>  s(      44SVV444444r   r   )defaultz.bak.u+    (BACKUP FAILED — file unchanged on disk))rv   r   r   rm   rn   r   r   r   rX   r   maxr   timer`   ra   
write_textstr)r4   rt   r   r   parsed	roundtrip
char_limitmax_entry_lendrift_detectedtsr   s              r   rz   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_)dirra   prefixwr]   r^   NzFailed to write memory file r   )r   rX   tempfilemkstempr  rb   osfdopenwriteflushfsyncrj   r   BaseExceptionunlinkrm   rn   RuntimeError)r   rO   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=)r'   r(   )r   N)r   r   )(__name__
__module____qualname____doc__r<   r   r5   r7   r   r  r   r=   rN   staticmethodr	   rI   r   r   rs   rv   boolr
   r~   r   r   r{   r   r   r   r   r   r   r   r   r   r   rJ   rE   rz   r   r   r   r   r%   r%   q   s         ,-(	) 	)# 	)s 	) 	) 	) 	)) ) ) )
tCH~ 
$sCx. 
 
 
 
.%
 %
 %
N !S	 !S !TRUY ! ! ! \!F ! ! ! ! ^ \!F %# %$ % % % \% AF   S  (SV-    ,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CAc CAS CAs CAtCQTH~ CA CA CA CAJ&@S &@C &@DcN &@ &@ &@ &@PiZ# iZ4S#X3G iZDQTVYQYN iZ iZ iZ iZV	3 	 	c3h 	 	 	 	(s (x} ( ( ( ( P P49 PS P$s) P P P \P  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%   c                  X   d} d}	 ddl m}  |            pi                     di           pi }t          |                    d|                     } t          |                    d|                    }n# t          $ r Y nw xY wt          | |          }|                                 |S )	u  Build a fresh on-disk :class:`MemoryStore`, honoring configured char limits.

    Use this from any context that has no live agent (the messaging gateway, the
    Desktop GUI, the bare CLI ``/memory`` handler) but still needs to read or
    apply approved memory writes. Mirrors how the live agent constructs its store
    in ``agent/agent_init.py`` — including the user's ``memory.memory_char_limit``
    / ``memory.user_char_limit`` overrides — so an approval applied without a live
    agent enforces the SAME caps as one applied with one.

    Falls back to the built-in defaults if config can't be loaded, so this can
    never raise on a missing/unreadable config.
    r'   r(   r   )load_configr.   r)   r*   )r)   r*   )hermes_cli.configr"  r   r   	Exceptionr%   rN   )r)   r*   r"  mem_cfgstores        r   load_on_disk_storer'    s     O111111;==&B++Hb99?R,?AR S STTgkk*;_MMNN    +'  E 
Ls   A.A5 5
BBr   rt   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/   user profiler.   r   zadd to r,   r   zreplace in zold: z
new: zremove from inline_summaryinline_detailFr   )r   rt   r   r   r   x   summaryoriginTidr   staged
pending_idr   ensure_ascii)toolsr*  r$  evaluate_gateMEMORYallowblocked
tool_errorr   stage_writecurrent_originjsondumps)r   rt   r   r   walabelr2  detaildecisionpayloadrecords              r   _apply_write_gaterJ  7  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   c           
         	 ddl m} n# t          $ r Y dS w xY w| dk    rdnd}dt          |           d| }g }|D ]}|pi }|                    d	d
          }|dk    r-|                    d|                    dd                      O|dk    rD|                    d|                    dd           d|                    dd                      |                    d| d|                    dd                      d                    |          }|                    |j        ||          }	|	j	        rdS |	j
        rt          |	j        d          S d| |d}
|                    |j        |
| d|dd          |                                          }t          j        dd|d         |	j        dd           S )!a  Evaluate the write gate for a batch of memory operations.

    Returns a JSON tool-result string when the batch should NOT proceed
    (blocked or staged), or None when the caller should perform the real
    batch write. The whole batch is gated as a single unit.
    r   r)  Nr/   r+  r.   zapply z
 op(s) to r   ?r   z
- remove: r   r,   r   z- replace: z -> r   z- r   r   r,  Fr/  batch)r   rt   r   r0  r1  Tr4  r5  r8  )r:  r*  r$  r   r   rU   rX   r;  r<  r=  r>  r?  r   r@  rA  rB  rC  )rt   r   rD  rE  r2  detail_linesr   r   rF  rG  rH  rI  s               r   _apply_batch_write_gaterO  p  sK   .......   tt %..NNHE9s:99%99GL E EX2ffXs##(?? ERVVJ-C-C E EFFFFI abffZ.D.D a a"&&QZ\^J_J_ a abbbb CS C CBFF9b,A,A C CDDDDYY|$$F	'QWXXH~ t ;(*E:::: F*MMG^^
	7,,fTcTl,,  ""   F
 :Dt$	& 	&   s   	 
r&  c           
          |                      |          }|                     |          }|                     |          }t          j        dd| d| d| d||dd|ddd	          S )
a  Build a recoverable error for a replace/remove call that arrived without
    ``old_text``.

    ``replace``/``remove`` are inherently targeted -- without ``old_text`` there
    is no entry to act on, so we cannot fulfil the call. But returning a bare
    "old_text is required" is a dead-end: some structured-output clients omit the
    optional ``old_text`` field (it isn't, and can't be, schema-required without
    a top-level combinator the Codex backend rejects -- see
    tests/tools/test_memory_tool_schema.py). So instead we return the current
    entry inventory plus an explicit retry instruction, letting the model reissue
    the call with ``old_text`` set to a unique substring of the entry it means.
    Mirrors the batch path's ``_batch_error`` shape. (issues #43412, #49466)
    F'z=' needs old_text -- a short unique substring of the entry to z!. None was provided. Reissue the z? with old_text set to part of one of the current_entries below.r   r   r   r8  )r   r   r   rB  rC  )r&  rt   r   rO   r   r   s         r   _missing_old_text_errorrR    s       ((G''Gf%%E:DF D DD D?ED D D  '---E---		
 		
    r   r.   c                 .   |t          dd          S |dvrt          d| dd          S |rft          |t                    st          dd          S t          ||          }||S |                    ||          }t          j        |d	          S | d
k    r|st          dd          S | dk    r1|r|s-|sdnd}|st          ||d          S t          | dd          S | dk    r|st          ||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 )aR  
    Single entry point for the memory tool. Dispatches to MemoryStore methods.

    Two shapes:
      - Single op: action + (content / old_text).
      - Batch:     operations=[{action, content?, old_text?}, ...] applied
                   atomically against the final char budget in ONE call.

    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'.zCoperations must be a list of {action, content?, old_text?} objects.r8  r   z%Content is required for 'add' action.r   r   r   z" is required for 'replace' action.r   zUnknown action 'z'. Use: add, replace, remove)r?  
isinstancerF   rO  r   rB  rC  rR  rJ  r   r   r   )	r   rt   r   r   r   r&  gate_resultresultmissings	            r   memory_toolrX    s   $ }fpuvvvv'''OVOOOY^____  6*d++ 	tcmrssss-fjAA"""6:66z&u5555
 wA5QQQQHG$,;**) 	E
 +5&)DDDWHHHRWXXXX(&ufh??? $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_requirementsrZ    s    4r   rH  c                    |                      d          }|                      dd          }|                      d          pd}|                      d          pd}|dk    r+|                    ||                      d          pg           S |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   rt   r.   r   r,   r   rM  r   r   r   r   FzUnknown staged action 'r   r   )r   r   r   r   r   )rH  r&  r   rt   r   r   s         r   apply_memory_pendingr\    s     [[""F[[8,,Fkk)$$*G{{:&&,"H  \)B)B)HbIIIyy)))}}VXw777||FH---'K'K'K'KLLLr   u  Save durable facts to persistent memory that survive across sessions. Memory is injected into every future turn, so keep entries compact and high-signal.

HOW: make ALL your changes in ONE call via an 'operations' array (each item: {action, content?, old_text?}). The batch applies atomically and the char limit is checked only on the FINAL result — so a single call can remove/replace stale entries to free room AND add new ones, even when an add alone would overflow. The response reports current/limit chars and confirms completion; one batch call finishes the update, so don't repeat it. Use the bare action/content/old_text fields only for a single lone change.

WHEN: save proactively when the user states a preference, correction, or personal detail, or you learn a stable fact about their environment, conventions, or workflow. Priority: user preferences & corrections > environment facts > procedures. The best memory stops the user repeating themselves.

IF FULL: an add is rejected with the current entries shown. Reissue as ONE batch that removes or shortens enough stale entries and adds the new one together.

TARGETS: 'user' = who the user is (name, role, preferences, style). 'memory' = your notes (environment, conventions, tool quirks, lessons).

SKIP: trivial/obvious info, easily re-discovered facts, raw data dumps, task progress, completed-work logs, temporary TODO state (use session_search for those). Reusable procedures belong in a skill, not memory.objectstring)r   r   r   zFThe action to perform (single-op shape). Omit when using 'operations'.)typeenumdescriptionr/   zIWhich memory store: 'memory' for personal notes, 'user' for user profile.zFThe entry content. Required for 'add' and 'replace' (single-op shape).)r_  ra  zREQUIRED for 'replace' and 'remove' (single-op shape): a short unique substring identifying the existing entry to modify. Omit only for 'add'.arrayzBatch shape: a list of operations applied atomically in one call against the final char budget. Preferred when making multiple changes or consolidating to make room. Each item is {action, content?, old_text?}.)r_  r`  zEntry content for add/replace.z3Substring identifying the entry for replace/remove.)r   r   r   )r_  
propertiesrequired)r_  ra  items)r   rt   r   r   r   )r"   ra  
parameters)registryr?  c           
         t          |                     dd          |                     dd          |                     d          |                     d          |                     d          |                    d          	          S )
Nr   r,   rt   r.   r   r   r   r&  )r   rt   r   r   r   r&  )rX  r   )argskws     r   <lambda>rk  m  sq    {xx"%%xx(++##*%%88L))ffWoo      r   u   🧠)r"   toolsetschemahandlercheck_fnemoji)r   r%   )Nr.   NNNN)/r  rB  loggingr  r  r  
contextlibr   pathlibr   hermes_constantsr   typingr   r   r	   r
   utilsr   rd   rc   ImportError	getLoggerr  rV   r   r   rS   r   r   r  r   r#   r%   r'  rJ  rO  rR  rX  r   rZ  r\  MEMORY_SCHEMAtools.registryrg  r?  registerr   r   r   <module>r|     s   0   				   % % % % % %       , , , , , , , , , , , , , , , , , ,             
LLLL   E   	 
	8	$	$* * * * *   P O O O O O:# :(3- : : : :
v  c3h    <c
K c
K c
K c
K c
K c
K c
K c
KL   @6c 63 6# 6 (62:3-6 6 6 6r,C ,T$sCx.5I ,hWZm , , , ,^= # s s    B 15#'E2 E2E2E2 E2 	E2
 d38n-.E2 K E2 	E2 E2 E2 E2P4    
M$sCx. M M4PSUXPX> M M M M. 	4,  !444g  !!6*j  !g 
 !  p 
  a
 %+3=Y=Y=Y"Z"Z,4Ee#f#f-5F{$|$|# #
 "*
  '$
 $
J JO( (1A AJ 0 / / / / / / /  	  '
     s5   A AAAAAAAA