Ë
    ãß²jÄk  ã                   óH  — d Z ddlZddlZddlZddlmZmZ ddlmZ ddl	m
Z
mZmZmZmZ ddlmZ ddlmZ  ej(                  d«      Zd	d
dœZdZdedee   fd„Z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
f   fd„Zdee   dedeee   ef   fd„Z G d„ d«      Z y)u!  MemoryStore â€” bounded, file-backed curated memory (MEMORY.md / USER.md).
Entries are joined by ``ENTRY_DELIMITER``; budgets are in chars (model-independent).
Module state that tests monkeypatch (``get_memory_dir``, ``fcntl``/``msvcrt``) stays
in ``tools.memory_tool`` and is read lazily.é    N)ÚcontextmanagerÚsuppress)ÚPath)ÚAnyÚDictÚListÚOptionalÚTuple)Úatomic_write_text)Úfirst_threat_messageztools.memory_toolzMEMORY (your personal notes)zUSER PROFILE (who the user is)©ÚmemoryÚuseru   
Â§
ÚcontentÚreturnc                 ó   — t        | d¬«      S )zœError string if *content* matches injection/exfil patterns. Strict scope:
    memory enters the system prompt, so a poisoned entry persists across sessions.Ústrict©Úscope)Ú_first_threat_message)r   s    ú</home/ubuntu/.hermes/hermes-agent/tools/memory_tool_store.pyÚ_scan_memory_contentr      s   € ô ! °Ô9Ð9ó    Úmessagec                 ó   — d| dœ|¥S )NF)ÚsuccessÚerror© )r   Úextras     r   Ú_errorr        s   € Ø wÑ8°%Ð8Ð8r   ÚpathÚbak_pathc                 ó@   — t        d| j                  › d|› d�|d¬«      S )zPExternal drift: the file wouldn't round-trip, so flushing would discard content.ú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).z´Open 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.)Údrift_backupÚremediation©r    Úname)r!   r"   s     r   Ú_drift_errorr)   $   s>   € äØ
˜TŸY™Y˜Kð (=à=E¸Jð Gdð	eð
 ð	fôhð hr   c                 ó4   — t        d| j                  › d�«      S )zNExisting-but-unreadable file: saving from an assumed-empty view would wipe it.r$   u=  : the file exists on disk but could not be read right now (temporarily locked by another program, a permission change, invalid/corrupt text encoding, or a filesystem error). Treating an unreadable file as empty and saving would wipe existing memory, so the write is refused. Nothing was changed â€” retry in a moment.r'   ©r!   s    r   Ú_read_failed_errorr,   1   s(   € äØ
˜TŸY™Y˜Kð (Vð 	WóXð Xr   ÚentriesÚold_textc                 ó  — t        | «      D ��cg c]  \  }}||k(  sŒ|‘Œ }}}|r|n"t        | «      D ��cg c]  \  }}||v sŒ|‘Œ c}}}t        |D �ch c]  }| |   ’Œ	 c}«      dkD  ry|r|d   dfS ddfS c c}}w c c}}w c c}w )uÄ  ``(index, ambiguous)`` for entries matching *old_text*. A whole-entry
    EXACT match (``old_text == entry``) takes absolute priority â€” substring
    matches are only considered when no entry equals *old_text*, so a short
    entry stays addressable even when its full text is contained inside a
    longer sibling entry (remove('test') vs '...tests pass...'). Exact-duplicate
    matches are safe (first wins); distinct matches â†’ ``(None, True)``.é   )NTr   NF)Ú	enumerateÚlen)r-   r.   ÚiÚeÚexactÚmatchess         r   Ú_find_unique_matchr7   :   s�   € ô % WÓ-×?‘4�1�a°°h³ŠQÐ?€EÑ?Ù‰e´¸7Ó1C×"U©¨¨AÀxÐSTÂ}¢1Ó"U€GÜ
 Ö(˜1ˆG�A‹JÒ(Ó)¨AÒ-ØÙ!ˆG�A‰J¨eÐ3Ð3 t¨eÐ3Ð3ùó	 @ùÛ"UùÚ(s   �A8�A8¶A>ÁA>ÁBc                   ój  — e Zd ZdZdZd:dddœdedededefd	„Zd
edefd„Z	d;d„Z
deeef   deeef   fd„Z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dee   fd„Zd
edefd„Zd
edefd„Zd
edefd„Zd
e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eef   fd„Zd d!œ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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d'e!e   deeef   fd*„Z"ed+ee   d,ed$ed&ed-ede#e!e   e!e   f   fd.„«       Z$d
ed/eeeef      deeef   fd0„Z%d
ede!e   fd1„Z&d<d
ededeeef   fd2„Z'd
edee   defd3„Z(edede#eef   fd4„«       Z)ed5edee   fd6„«       Z*ededee   fd7„«       Z+ededee   fd8„«       Z,d
ed5ede!e   fd9„Z-y)=ÚMemoryStorezáBounded curated memory with file persistence; one instance per AIAgent.
    ``_system_prompt_snapshot`` is frozen at load time (prefix-cache stable);
    ``memory_entries`` / ``user_entries`` are live state persisted to disk.é   T)Úmemory_enabledÚuser_profile_enabledÚmemory_char_limitÚuser_char_limitr;   r<   c                ó~   — g | _         g | _        ||c| _        | _        ||c| _        | _        dddœ| _        d| _        y )NÚ r   r   )Úmemory_entriesÚuser_entriesr=   r>   r;   r<   Ú_system_prompt_snapshotÚ_consolidation_failures)Úselfr=   r>   r;   r<   s        r   Ú__init__zMemoryStore.__init__X   sL   € à)+ˆÔØ')ˆÔØ7HÈ/Ð4ˆÔ Ô 4Ø9GÐI]Ð6ˆÔ˜TÔ6ØBDÈbÑ7QˆÔ$Ø'(ˆÕ$r   Útargetr   c                 ó<   — |dk(  r| j                   S | j                  S ©Nr   )r<   r;   ©rE   rG   s     r   Útarget_enabledzMemoryStore.target_enabledc   s    € Ø,2°fÒ,<ˆt×(Ñ(ÐUÀ$×BUÑBUÐUr   Nc                 ó   — d| _         y)zCall at turn start.r   N)rD   )rE   s    r   Úreset_consolidation_failuresz(MemoryStore.reset_consolidation_failuresf   s
   € à'(ˆÕ$r   Úresponsec                 óŠ   — | xj                   dz  c_         | j                   | j                  k  r|S ddd| j                   › d�dœS )u§  Count a consolidation failure: under the per-turn cap return ``response``
        (it says how to retry); past it a TERMINAL result so the model stops looping.

        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).
        r0   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   )rD   Ú$_MAX_CONSOLIDATION_FAILURES_PER_TURN)rE   rN   s     r   Ú_consolidation_failurez"MemoryStore._consolidation_failurej   sV   € ð 	×$Ò$¨Ñ)Õ$Ø×'Ñ'¨4×+TÑ+TÒTØˆOØ ¨$Ø*¨4×+GÑ+GÐ*Hð I5ð 5ñ7ð 	7r   c                 ó  ‡	— ddl mŠ	 ˆ	fd„}dD ]ñ  }| j                  |«      }ddlm}  ||j
                  «       t        t        j                  | j                  |«      «      «      }| j                  ||«       | j                  |«      x}| j                  |«      x}kD  r"t        j                  d|j                  ||«       | j!                  ||D �cg c]  } |||j                  «      ‘Œ c}«      | j"                  |<   Œó yc c}w )u/  Load MEMORY.md / USER.md and capture the frozen system-prompt snapshot.
        Threat hits are replaced by a ``[BLOCKED: â€¦]`` placeholder in the SNAPSHOT only;
        live lists keep the raw text so the user can see and remove poisoned entries
        (dropping them silently would hide the attack).r   )Úscan_for_threatsc                 óÆ   •— | r| j                  d«      s
 ‰| d¬«      nd }|s| S t        j                  d|dj                  |«      «       d|› ddj                  |«      › d�S )	Nz	[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.])Ú
startswithÚloggerÚwarningÚjoin)ÚentryÚfilenameÚfindingsrT   s      €r   Ú	_sanitizez-MemoryStore.load_from_disk.<locals>._sanitize�   sv   ø€ áBGÐPU×P`ÑP`ÐalÔPmÑ'¨°XÕ>ÐswˆHÙØ�Ü�N‰NÐJÈHÐVZ×V_ÑV_Ð`hÓViÔjØ   
Ð*NÈtÏyÉyÐYaÓObÐNcð deð fð gr   r   ©Úmkdir_under_hermes_homez„%s exceeds its char limit on load: %d/%d chars. Entries stay loaded; further additions are blocked until it is back under the limit.N)Útools.threat_patternsrT   Ú	_path_forÚhermes_constantsr_   ÚparentÚlistÚdictÚfromkeysÚ
_read_fileÚ_set_entriesÚ_char_countÚ_char_limitrW   rX   r(   Ú_render_blockrC   )
rE   r]   rG   r!   r_   r-   ÚcountÚlimitr4   rT   s
            @r   Úload_from_diskzMemoryStore.load_from_diskz   sô   ø€ õ
 	;ô	gð )ò 	zˆFØ—>‘> &Ó)ˆDÝ@á# D§K¡KÔ0äœ4Ÿ=™=¨¯©¸Ó)>Ó?Ó@ˆGØ×Ñ˜f gÔ.ð ×)Ñ)¨&Ó1Ð1�¸t×?OÑ?OÐPVÓ?WÐ6W°eÒXÜ—‘ð  aà#Ÿy™y¨%°ô8ð 48×3EÑ3EÀfÐpwÖNxÐklÉyÐYZÐ\`×\eÑ\eÕOfÒNxÓ3yˆD×(Ñ(¨Ò0ñ	zùò Oys   ÃDr!   c           	   #   óf  ‡‡‡	K  — ddl m} |j                  |j                  cŠŠ	| j	                  | j
                  dz   «      }ddlm}  ||j                  «       ‰€‰	€d–— yt        j                  t        j                  z  }t        t        d«      r|t        j                  z  }t        j                  ||d«      }	 t        t        d«      rt        j                  |d«       t        j                   |d	d
¬«      Š‰5  dt&        fˆˆˆ	fd„} |d«       	 d–— t)        t*        «      5   |d«       ddd«       ddd«       y# t"        $ r t        j$                  |«       ‚ w xY w# 1 sw Y   Œ5xY w# t)        t*        «      5   |d«       ddd«       w # 1 sw Y   w xY wxY w# 1 sw Y   yxY w­w)zkExclusive lock on a separate .lock file so the memory file itself can
        still be atomically replaced.r   ©Úmemory_toolz.lockr^   NÚ
O_NOFOLLOWi€  Úfchmodzr+úutf-8©ÚencodingÚunlockc                 óþ   •— ‰r0‰j                  ‰| r‰j                  «       y ‰j                  «       y ‰j                  d«       ‰j	                  ‰j                  «       | r‰j                  n‰j                  d«       y )Nr   r0   )ÚflockÚLOCK_UNÚLOCK_EXÚseekÚlockingÚfilenoÚLK_UNLCKÚLK_LOCK)rw   ÚfcntlÚfdÚmsvcrts    €€€r   Ú_flockz&MemoryStore._file_lock.<locals>._flock¹   sQ   ø€ ÙØ—K‘K ±V E§M¡MÕOÀÇÁÕOà—G‘G˜A”JØ—N‘N 2§9¡9£;Á6°·²ÈvÏ~É~Ð_`Õar   FT)Útoolsrq   r�   rƒ   Úwith_suffixÚsuffixrb   r_   rc   ÚosÚO_RDWRÚO_CREATÚhasattrrr   Úopenrs   ÚfdopenÚ	ExceptionÚcloseÚboolr   ÚOSError)
r!   Ú_mtÚ	lock_pathr_   ÚflagsÚraw_fdr„   r�   r‚   rƒ   s
          @@@r   Ú
_file_lockzMemoryStore._file_lock›   si  úè ø€ õ
 	-ØŸ	™	 3§:¡:ˆˆˆvØ×$Ñ$ T§[¡[°7Ñ%:Ó;ˆ	Ý<á 	× 0Ñ 0Ô1Øˆ=˜V˜^ÛØÜ—	‘	œBŸJ™JÑ&ˆÜ”2�|Ô$Ø”R—]‘]Ñ"ˆEÜ—‘˜ E¨5Ó1ˆð
	ô
 ”r˜8Ô$Ü—	‘	˜& %Ô(Ü—‘˜6 4°'Ô:ˆBð ñ 	!ðbœt÷ bñ �5ŒMð!ÛäœgÓ&ñ !Ù˜4”L÷!÷	!ð 	!øô ò 	Ü�H‰H�VÔØð	ú÷!ð !û”XœgÓ&ñ !Ù˜4”L÷!÷ !ñ !ý÷	!ð 	!üs~   …B:F1Ã >E Ã>F1Ä F%ÄE4ÄF%Ä+	E(Ä4F%Ä<	F1Å E%Å%F1Å(E1	Å-F%Å4F"Æ	F	Æ	F"ÆFÆF"Æ"F%Æ%F.Æ*F1c                 óF   — ddl m} |j                  «       | dk(  rdz  S dz  S )Nr   rp   r   zUSER.mdz	MEMORY.md)r…   rq   Úget_memory_dir)rG   rq   s     r   ra   zMemoryStore._path_forÆ   s)   € å%Ø×)Ñ)Ó+¸FÀfÒ<L¨yÑ^Ð^ÐR]Ñ^Ð^r   c                 ó<   — |dk(  r| j                   S | j                  S rI   )rB   rA   rJ   s     r   Ú_entries_forzMemoryStore._entries_forË   s    € Ø$*¨fÒ$4ˆt× Ñ ÐM¸$×:MÑ:MÐMr   r-   c                 ó8   — t        | |dk(  rd|«       y d|«       y )Nr   rB   rA   )Úsetattr)rE   rG   r-   s      r   rh   zMemoryStore._set_entriesÎ   s   € Ü�¨°&Ò(8�nÐPWÕXÐ>NÐPWÕXr   c                 ó\   — t        t        j                  | j                  |«      «      «      S ©N)r2   ÚENTRY_DELIMITERrY   rš   rJ   s     r   ri   zMemoryStore._char_countÑ   s#   € Ü”?×'Ñ'¨×(9Ñ(9¸&Ó(AÓBÓCÐCr   c                 ó<   — |dk(  r| j                   S | j                  S rI   )r>   r=   rJ   s     r   rj   zMemoryStore._char_limitÔ   s    € Ø'-°Ò'7ˆt×#Ñ#ÐS¸T×=SÑ=SÐSr   c                 óP   — | j                  |«      d›d| j                  |«      d›�S )Nú,ú/)ri   rj   rJ   s     r   Ú_usagezMemoryStore._usage×   s/   € Ø×"Ñ" 6Ó*¨1Ð-¨Q¨t×/?Ñ/?ÀÓ/GÈÐ.JÐKÐKr   Úcurrentc                 ó~   — | j                  |«      }|dkD  rt        dt        ||z  dz  «      «      nd› d|d›d|d›d�S )Nr   éd   u   % â€” r¢   r£   z chars)rj   ÚminÚint)rE   rG   r¥   rm   s       r   Ú
_usage_pctzMemoryStore._usage_pctÚ   sU   € Ø× Ñ  Ó(ˆØ<AÀAºI”#�cœ3 ¨%¡°3Ñ6Ó7Ô8È1ÐMÈVÐT[Ð\]ÐS^Ð^_Ð`eÐfgÐ_hÐhnÐoÐor   r   c           	      óx   — | j                  t        || j                  |«      | j                  |«      ¬«      «      S )zMConsolidation failure carrying the live entries so the model can consolidate.)Úcurrent_entriesÚusage)rR   r    rš   r¤   ©rE   rG   r   s      r   Ú_failure_with_entriesz!MemoryStore._failure_with_entriesÞ   s8   € à×*Ñ*Ü�7¨D×,=Ñ,=¸fÓ,EÈTÏ[É[ÐY_ÓM`Ôaócð 	cr   c                 ó^   — | j                  t        |dz   | j                  |«      ¬«      «      S )zåBatch-abort failure WITHOUT ``current_entries``: the store did not change and the
        caller already holds the inventory, so echoing it made each consolidation retry
        grow the context it was invoked to shrink (#97316).z6 No operations were applied (batch is all-or-nothing).)r­   )rR   r    r¤   r®   s      r   Ú_batch_failurezMemoryStore._batch_failureã   s6   € ð ×*Ñ*Ü�7ÐUÑUÐ]a×]hÑ]hÐioÓ]pÔqósð 	sr   F©Ú
skip_driftr³   c                óú  — | j                  |«      }| j                  |«      5  | j                  |«      \  }}|st        |«      cddd«       S |rdn| j	                  ||«      }| j                  |t        t        j                  | j                  |«      «      «      «       |rt        ||«      cddd«       S  || j                  |«      | j                  |«      «      }t        |t        «      r|cddd«       S | j                  ||d   «       ddlm}	  |	|j                   «       | j#                  ||d   «       t%        |«      dkD  r|d   ni }
 | j&                  ||d   fi |
¤Žcddd«       S # 1 sw Y   yxY w)u¤  Lock, re-read from disk, run ``mutate(entries, limit)`` -> ``(new_entries, message)``
        or an error dict, then persist and return the success response. The reload aborts
        on an existing-but-unreadable file (even append-only ``add`` rewrites the whole
        file) and, unless *skip_drift*, on external drift (flushing would discard
        un-roundtrippable content). Drift check and parse use the SAME raw snapshot â€”
        a failed second read used to count as "no drift". The closure may return a
        third value, a dict merged into the success payload (``_error``'s ``**extra``
        convention) â€” e.g. the full text a replace overwrote (#117952).Nr   r^   é   r0   )ra   r–   Ú_read_raw_checkedr,   Ú_detect_external_driftrh   rd   re   rf   Ú_parse_entriesr)   rš   rj   Ú
isinstancerb   r_   rc   Ú_write_filer2   Ú_success_response)rE   rG   Úmutater³   r!   ÚrawÚread_okÚbakÚresultr_   Úextra_fieldss              r   Ú_mutatezMemoryStore._mutateê   sb  € ð �~‰~˜fÓ%ˆØ�_‰_˜TÓ"ñ 	MØ×1Ñ1°$Ó7‰LˆC�ÙÜ)¨$Ó/÷	Mñ 	Mñ %‘$¨$×*EÑ*EÀfÈcÓ*RˆCØ×Ñ˜f¤d¬4¯=©=¸×9LÑ9LÈSÓ9QÓ+RÓ&SÔTÙÜ# D¨#Ó.÷	Mñ 	Mñ ˜D×-Ñ-¨fÓ5°t×7GÑ7GÈÓ7OÓPˆFÜ˜&¤$Ô'Ø÷	Mñ 	Mð ×Ñ˜f f¨Q¡iÔ0Ý@á# D§K¡KÔ0Ø×Ñ˜T 6¨!¡9Ô-Ü(+¨F«°aª˜6 !š9¸RˆLØ)�4×)Ñ)¨&°&¸±)ÑL¸|ÑL÷#	M÷ 	Mò 	Mús   £!E1ÁA E1Â88E1Ã:A-E1Å1E:r   c                 ó¨   ‡ ‡‡— ‰j                  «       Š‰st        d«      S t        ‰«      x}rt        |«      S ˆˆ ˆfd„}‰ j                  ‰|d¬«      S )zDAppend a new entry. Returns error if it would exceed the char limit.zContent cannot be empty.c                 óö   •— ‰| v r‰j                  ‰d«      S t        t        j                  | ‰gz   «      «      |kD  r6‰j	                  ‰d‰j                  ‰«      d›d|d›dt        ‰«      › d�«      S | ‰gz   dfS )Nz*Entry already exists (no duplicate added).z
Memory at r¢   r£   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.zEntry added.)r»   r2   rŸ   rY   r¯   ri   )r-   rm   r   rE   rG   s     €€€r   Ú_addzMemoryStore.add.<locals>._add  s¡   ø€ Ø˜'Ñ!Ø×-Ñ-¨fÐ6bÓcÐcÜ”?×'Ñ'¨°7°)Ñ(;Ó<Ó=ÀÒEØ×1Ñ1°&Ø  ×!1Ñ!1°&Ó!9¸!Ð <¸A¸eÀA¸Yð GÜ˜G›�~ð &XðYó[ð [ð
 ˜g˜YÑ&¨Ð6Ð6r   Tr²   )Ústripr    r   rÂ   )rE   rG   r   Ú
scan_errorrÅ   s   ```  r   ÚaddzMemoryStore.add  sT   ú€ à—-‘-“/ˆÙÜÐ4Ó5Ð5Ü-¨gÓ6Ð6ˆ:Ð6Ü˜*Ó%Ð%ö		7ð �|‰|˜F D°Tˆ|Ó:Ð:r   r.   Únew_contentc                 óä   — |j                  «       }|j                  «       st        d«      S |st        d«      S t        |«      x}rt        |«      S | j                  ||j                  «       |«      S )uÍ   Find the entry containing old_text (whole-entry exact match first) and
        replace the WHOLE entry with new_content â€” old_text only locates the entry;
        the matched span is not spliced into it.úold_text cannot be empty.z<new_content cannot be empty. Use 'remove' to delete entries.)rÆ   r    r   Ú_edit)rE   rG   r.   rÉ   rÇ   s        r   ÚreplacezMemoryStore.replace  sl   € ð "×'Ñ'Ó)ˆØ�~‰~ÔÜÐ5Ó6Ð6ÙÜÐXÓYÐYÜ-¨kÓ:Ð:ˆ:Ð:Ü˜*Ó%Ð%Ø�z‰z˜& (§.¡.Ó"2°KÓ@Ð@r   c                 óz   — |j                  «       st        d«      S | j                  ||j                  «       d«      S )z/Remove the entry containing old_text substring.rË   N)rÆ   r    rÌ   )rE   rG   r.   s      r   ÚremovezMemoryStore.remove*  s2   € à�~‰~ÔÜÐ5Ó6Ð6Ø�z‰z˜& (§.¡.Ó"2°DÓ9Ð9r   c                 ó>   ‡ ‡‡‡— ˆˆˆ ˆfd„}‰ j                  ‰|«      S )zWLocked replace (``new_content`` set) or remove (None) of the entry matching *old_text*.c           
      ó¾  •— t        | ‰«      \  }}|r:t        d‰› d�| D �cg c]  }‰|v sŒ|d d t        |«      dkD  rdndz   ‘Œ! c}¬«      S |€'‰	j                  t        d‰› d‰rd	nd
› d�| ¬«      «      S | d | ‰€g n‰gz   | |dz   d  z   }‰€|dfS t        t        j                  |«      «      }||kD  r‰	j                  ‰
d|d›d|d›d�«      S |dd| |   ifS c c}w )NzMultiple entries matched 'z'. Be more specific.éP   z...r@   )r6   zNo entry matched 'zV'. Check current_entries below and retry with the exact text of the entry you want to rÍ   rÏ   ú.)r¬   r0   zEntry removed.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.zEntry replaced.Úreplaced_entry)r7   r    r2   rR   rŸ   rY   r¯   )r-   rm   ÚidxÚ	ambiguousr4   ÚreplacedÚ	new_totalrÉ   r.   rE   rG   s          €€€€r   Ú_applyz!MemoryStore._edit.<locals>._apply2  sX  ø€ Ü/°¸ÓB‰NˆC�ÙÜÐ :¸8¸*ÐDXÐYØX_Ö&qÐSTÐckÐopÒcp q¨¨" v¼#¸a»&À2º+±È2Ó'NÒ&qôsð sàˆ{Ø×2Ñ2´6Ø(¨¨
ð 30Ù=H±	ÈhÐ/WÐWXðZØkrô4tó uð uð ˜t �}¨kÐ.A©ÈÀ}ÑUÐX_Ð`cÐfgÑ`gÐ`hÐXiÑiˆHØÐ"ØÐ!1Ð1Ð1ÜœO×0Ñ0°Ó:Ó;ˆIØ˜5Ò Ø×1Ñ1°&Ø6°yÀ°mÀ1ÀUÈ1ÀIð N?ð @óBð Bð Ð.Ð1AÀ7È3Á<Ð0PÐPÐPùò 'rs
   ¡	C
«C
)rÂ   )rE   rG   r.   rÉ   rÙ   s   ```` r   rÌ   zMemoryStore._edit0  s   û€ ÷	Qð& �|‰|˜F FÓ+Ð+r   ÚworkingÚactÚposc                 ó  — |dk(  r|s|› d�dfS || vr| j                  |«       y|dvr|› d�dfS |s|› d�dfS |dk(  r	|s|› d	�dfS t        | |«      \  }}|r
|› d
|› d�dfS |€
|› d|› d�dfS |dk(  r| |   nd}|dk(  r|gng | ||dz    d|fS )aD  Apply one batch op to *working* in place; return ``(error message, replaced
        entry text)``. The first element is None on every success path; the second is
        the full entry a 'replace' overwrote (None on non-replace paths and on every
        error path), surfaced so the caller can show what was lost (#117952).rÈ   z: content is required.N)NN)rÍ   rÏ   z.: unknown action. Use add, replace, or remove.z: old_text is required.rÍ   z6: content is required (use action='remove' to delete).z: 'z8' matched multiple distinct entries -- be more specific.z: no entry matched 'z'.r0   )Úappendr7   )rÚ   rÛ   r   r.   rÜ   rÕ   rÖ   Úreplaced_texts           r   Ú_apply_batch_opzMemoryStore._apply_batch_opG  s  € ð �%Š<ÙØ˜Ð4Ð5°tÐ;Ð;Ø˜gÑ%Ø—‘˜wÔ'ØØÐ+Ñ+Ø�UÐHÐIÈ4ÐOÐOÙØ�UÐ1Ð2°DÐ8Ð8Ø�)Ò¡GØ�UÐPÐQÐSWÐWÐWÜ+¨G°XÓ>‰ˆˆYÙØ�U˜#˜h˜ZÐ'_Ð`ÐbfÐfÐfØˆ;Ø�UÐ.¨x¨j¸Ð;¸TÐAÐAØ(+¨yÒ(8˜ š¸dˆØ,/°9Ò,< ™yÀ"ˆ��C˜!‘GÐØ�]Ð"Ð"r   Ú
operationsc                 óL  ‡ ‡‡‡— ‰st        d«      S ‰D �cg c]  }|xs i ‘Œ
 c}Št        ‰«      D ]S  \  }}|j                  d«      dv xr! |j                  d«      xr t        |d   «      }|sŒ?t        d|dz   › d|› �«      c S  ˆˆˆ ˆfd„}‰ j	                  ‰|«      S c c}w )	uX  Apply add/replace/remove ops atomically against the FINAL budget, so one call
        can free space and add entries. All-or-nothing: any malformed / unmatched op or
        an over-limit result writes NOTHING and returns the first failure. Aborts do not
        echo ``current_entries`` â€” the store is unchanged and the model already has it.zoperations list is empty.Úaction>   rÈ   rÍ   r   ú
Operation r0   ú: c                 óÐ  •— t        | «      }i }t        ‰«      D ]²  \  }}|j                  d«      }‰j                  |||j                  d«      xs |j                  d«      xs dj	                  «       |j                  d«      xs dj	                  «       d|dz   › d|xs d	› d
�«      \  }}|r‰j                  ‰|«      c S |€Œ«|||dz   <   Œ´ | r3|s1‰j                  ‰«      j                  }	‰j                  ‰d|	› d�«      S t        t        j                  |«      «      }
|
|kD  r'‰j                  ‰dt        ‰«      › d|
d›d|d›d�«      S |rd|ini }|dt        ‰«      › d�|fS )Nrã   r   Únew_textr@   r.   rä   r0   z (Úunknownú)zRefusing to empty uö   : this batch would remove every entry from a previously non-empty store. Keep at least one entry â€” merge overlapping entries into a shorter one instead of removing the last one. To delete the final entry deliberately, use single remove() calls.zAfter applying all z  operations, memory would be at r¢   r£   zW chars -- over the limit. Remove or shorten more entries in the same batch, then retry.Úreplaced_entrieszApplied z operation(s).)rd   r1   Úgetrà   rÆ   r±   ra   r(   r2   rŸ   rY   )r-   rm   rÚ   r×   r3   ÚoprÛ   Úmsgrß   ÚlabelrØ   Úreplaced_fieldsrá   ÚopsrE   rG   s               €€€€r   rÙ   z'MemoryStore.apply_batch.<locals>._applyq  s¯  ø€ Ü˜7“mˆGØˆHÜ" 3›ò 
4‘��2Ø—f‘f˜XÓ&�Ø%)×%9Ñ%9Ø˜S 2§6¡6¨)Ó#4Ò#P¸¿¹¸zÓ8JÒ#PÈb×"WÑ"WÓ"YØ—V‘V˜JÓ'Ò-¨2×4Ñ4Ó6¸*ÀQÈÁUÀGÈ2ÈcÒN^ÐU^ÐM_Ð_`Ð8aó&cÑ"��]ñ Ø×.Ñ.¨v°sÓ;Ò;Ø Ñ,ð '4�H˜Q ™U’Oð
4ñ ™wð Ÿ™ vÓ.×3Ñ3�Ø×*Ñ*¨6Ø(¨¨ð 0Kð LóNð Nô
 œO×0Ñ0°Ó9Ó:ˆIØ˜5Ò Ø×*Ñ*¨6Ø)¬#¨j«/Ð):Ð:ZØ  �m 1 U¨1 Ið .=ð>ó@ð @ñ AIÐ1°8Ñ<ÈbˆOØ˜h¤s¨:£Ð&7°~ÐFÈÐWÐWr   )r    r1   rë   r   rÂ   )rE   rG   rá   rì   r3   rÇ   rÙ   rð   s   ```    @r   Úapply_batchzMemoryStore.apply_batchc  s¶   û€ ñ
 ÜÐ5Ó6Ð6Ø",Ö-˜BˆrŠx�R‰xÒ-ˆä˜s“^ò 	B‰EˆAˆrØŸ™ Ó)Ð-?Ð?Ò}ÀBÇFÁFÈ9ÓDUÒ}ÔZnÐoqÐr{Ño|ÓZ}ˆJÚÜ 
¨1¨q©5¨'°°J°<Ð@ÓAÒAð	B÷
	Xð@ �|‰|˜F FÓ+Ð+ùòO .s   –B!c                 óB   — | j                   j                  |d«      xs dS )u…   Frozen load-time snapshot (NOT live state â€” mid-session writes don't touch
        it, preserving the prefix cache); None if empty.r@   N)rC   rë   rJ   s     r   Úformat_for_system_promptz$MemoryStore.format_for_system_prompt“  s"   € ð ×+Ñ+×/Ñ/°¸Ó;ÒC¸tÐCr   c           	      óª   — d| _         dd|| j                  || j                  |«      «      t        | j	                  |«      «      dœ|rd|ini ¥|¥ddi¥S )u¢  TERMINAL and WITHOUT the entries list: echoing entries invites the model to
        "find more to fix" and re-issue the same ops. A successful write resets the
        per-turn failure budget. ``**extra`` mirrors ``_error``'s convention â€” e.g. the
        full text a replace overwrote (#117952), deliberately visible despite the
        no-entries rule: silent data loss is the failure this field exists to prevent.r   T)r   rP   rG   r­   Úentry_countr   Únoteu:   Write saved. This update is complete â€” do not repeat it.)rD   rª   ri   r2   rš   )rE   rG   r   r   s       r   r»   zMemoryStore._success_response˜  sw   € ð ()ˆÔ$Ø¨¸ØŸ™¨°×1AÑ1AÀ&Ó1IÓJÜ" 4×#4Ñ#4°VÓ#<Ó=ñVáZaÀ9ÈgÑBVÐgiðVð ðVð ÐTñ	Vð 	Vr   c           
      ó¨   — |syt         j                  |«      d}}t        |dk(  rdnd   }|› d|› d| j                  |t	        |«      «      › d|› d|› �	S )zHSystem prompt block: header + usage indicator + entries ("" when empty).r@   uŠ   â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�r   r   Ú
z [z]
)rŸ   rY   ÚMEMORY_BLOCK_HEADERSrª   r2   )rE   rG   r-   r   ÚsepÚtitles         r   rk   zMemoryStore._render_block§  se   € áØÜ&×+Ñ+¨GÓ4°j�ˆÜ$¨v¸Ò/?¡VÀXÑNˆØ��b˜˜˜r $§/¡/°&¼#¸g»,Ó"GÐ!HÈÈCÈ5ÐPRÐSZÐR[Ð\Ð\r   c                 óx   — | j                  «       sy	 | j                  d¬«      dfS # t        t        f$ r Y yw xY w)a   ``(raw, read_ok)``; ``read_ok`` is False ONLY when the file EXISTS but can't be
        read. Decoding stays STRICT (``errors="replace"`` would hand callers a lossy view
        a save then persists); ``utf-8-sig`` strips a Notepad BOM off the first entry.)r@   Tz	utf-8-sigru   T)r@   F)ÚexistsÚ	read_textr‘   ÚUnicodeDecodeErrorr+   s    r   r¶   zMemoryStore._read_raw_checked¯  sE   € ð
 �{‰{Œ}Øð		ð —>‘>¨;�>Ó7¸Ð=Ð=øÜÔ+Ð,ò 	Ùð	ús   “' §9¸9r½   c                 óf   — d„ | j                  t        «      D «       D �cg c]  }|sŒ|‘Œ	 c}S c c}w )uR   Stripped, non-empty entries; splits on the FULL delimiter so a bare "Â§" survives.c              3   ó<   K  — | ]  }|j                  «       –— Œ y ­wrž   )rÆ   )Ú.0Úxs     r   ú	<genexpr>z-MemoryStore._parse_entries.<locals>.<genexpr>Ä  s   è ø€ ÒJ¨!˜AŸG™GŸIÑJùs   ‚)ÚsplitrŸ   )r½   r4   s     r   r¸   zMemoryStore._parse_entriesÁ  s)   € ñ K¨s¯y©y¼Ó/IÔJÖP�aÊa’ÒPÐPùÒPs   Ÿ.§.c                 óX   — t         j                  t         j                  | «      d   «      S )z¨Entries of a memory file ([] on any error). Read-only callers only; mutation
        paths use ``_read_raw_checked`` so they can refuse to overwrite an unreadable file.r   )r9   r¸   r¶   r+   s    r   rg   zMemoryStore._read_fileÆ  s%   € ô ×)Ñ)¬+×*GÑ*GÈÓ*MÈaÑ*PÓQÐQr   c                 óŒ   — 	 t        | t        j                  |«      d¬«       y# t        $ r}t	        d| › d|› �«      ‚d}~ww xY w)zpAtomic temp-file + rename: readers never see a truncated file. Also used by
        agent/learning_mutations.py.z.mem_)Ú
tmp_prefixzFailed to write memory file rå   N)r   rŸ   rY   r‘   ÚRuntimeError)r!   r-   r4   s      r   rº   zMemoryStore._write_fileÌ  sJ   € ð	KÜ˜d¤O×$8Ñ$8¸Ó$AÈgÖVøÜò 	KÜÐ!=¸d¸VÀ2ÀaÀSÐIÓJÐJûð	Kús   ‚!$ ¤	A­>¾Ac                 óþ  — | j                  |«      }|j                  «       rS|j                  «       t        j                  |«      k(  r.t	        t        t        |«      d¬«      | j                  |«      k  ry| j                  |«      }|j                  |j                  dt        t        j                  «       «      › �z   «      }	 |j                  |d¬«       t        |«      S # t        $ r t        |«      dz   cY S w xY w)ué   ``.bak.<ts>`` snapshot path if *raw* shows external drift, else None. Signals:
        round-trip mismatch, or one entry over the whole-file limit (no tool-written
        entry can be â€” an external writer appended free-form text).r   )ÚdefaultNz.bak.rt   ru   u+    (BACKUP FAILED â€” file unchanged on disk))r¸   rÆ   rŸ   rY   ÚmaxÚmapr2   rj   ra   r†   r‡   r©   ÚtimeÚ
write_textr‘   Ústr)rE   rG   r½   Úparsedr!   r"   s         r   r·   z"MemoryStore._detect_external_driftÕ  sÜ   € ð ×$Ñ$ SÓ)ˆØ�y‰yŒ{˜sŸy™y›{¬o×.BÑ.BÀ6Ó.JÒJÜ#&¤s¬3°Ó'7ÀÔ#CÀt×GWÑGWÐX^ÓG_Ò#_ØØ�~‰~˜fÓ%ˆØ×#Ñ# D§K¡K°E¼#¼d¿i¹i»kÓ:JÐ9KÐ2LÑ$LÓMˆð	QØ×Ñ ¨gÐÔ6ô �8‹}Ðøô ò 	QÜ�x“=Ð#PÑPÒPð	Qús   ÃC" Ã"C<Ã;C<)i˜  i_  )r   Nrž   ).Ú__name__Ú
__module__Ú__qualname__Ú__doc__rQ   r©   r�   rF   r  rK   rM   r   r   rR   rn   Ústaticmethodr   r   r–   ra   r   rš   rh   ri   rj   r¤   rª   r¯   r±   rÂ   rÈ   rÍ   rÏ   r	   rÌ   r
   rà   rñ   ró   r»   rk   r¶   r¸   rg   rº   r·   r   r   r   r9   r9   M   s4  „ ñOð ,-Ð(ð)Ø(,È4ò)¨#ð )Àsð )Ø!%ð)ØDHó)ðV Sð V¨Tó Vó)ð7¨t°C¸°H©~ð 7À$ÀsÈCÀxÁ.ó 7ò zðB Øð'!˜ò '!ó ó ð'!ðR ð_˜#ð _ $ò _ó ð_ðN 3ð N¨4°©9ó NðY 3ð Y°°c±ó YðD #ð D¨#ó DðT #ð T¨#ó TðL˜Sð L Só Lðp ð p¨sð p°só pðc¨Cð c¸#ð cÀ$ÀsÈCÀxÁ.ó cð
s Sð s°3ð s¸4ÀÀSÀ¹>ó sð BGò M˜cð M¸$ð MÈ4ÐPSÐUXÐPXÉ>ó Mð:;˜#ð ;¨ð ;°°S¸#°X±ó ;ð,A˜cð A¨Sð A¸sð AÀtÈCÐQTÈHÁ~ó Að:˜Sð :¨Cð :°D¸¸c¸±Nó :ð,˜Cð ,¨3ð ,¸XÀc¹]ð ,ÈtÐTWÐY\ÐT\É~ó ,ð. ð#  c¡ð #°ð #¸sð #Ècð #Ø ð#Ø%*¨8°C©=¸(À3¹-Ð+GÑ%Hò#ó ð#ð6., #ð .,°4¸¸SÀ#¸X¹Ñ3Gð .,ÈDÐQTÐVYÐQYÉNó .,ð`D¨sð D°xÀ±}ó Dñ
V¨ð V°cð VÈdÐSVÐX[ÐS[Énó Vð] Cð ]°$°s±)ð ]Àó ]ð ð ð ¨¨s°D¨yÑ)9ò ó ðð" ðQ˜Cð Q D¨¡Iò Qó ðQð ðR˜ð R $ s¡)ò Ró ðRð
 ðK˜$ð K¨¨c©ò Kó ðKð¨Sð °sð ¸xÈ¹}ô r   r9   )!r  Úloggingrˆ   r  Ú
contextlibr   r   Úpathlibr   Útypingr   r   r   r	   r
   Úutilsr   r`   r   r   Ú	getLoggerrW   rù   rŸ   r  r   r    r)   r,   r©   r�   r7   r9   r   r   r   ú<module>r     s  ðñ0ó
 Û 	Û ß /Ý ß 3Õ 3å #Ý Oà	ˆ×	Ñ	Ð.Ó	/€ð
 -Ð6VñXÐ ð €ð: #ð :¨(°3©-ó :ð9�Cð 9 T¨#¨s¨(¡^ó 9ð
h�tð 
h sð 
h¨t°C¸°H©~ó 
hðX˜Tð X d¨3°¨8¡nó Xð4  S¡	ð 4°Sð 4¸UÀ8ÈCÁ=ÐRVÐCVÑ=Wó 4÷&Vò Vr   