
    iA0                       d Z ddlm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mZ eeeef         ZdZdZ G d	 d
          Z e            Zd-dZd.dZd/dZd0dZddd1dZd2d Zd3d"Zd4d#Z d5d)Z!d6d+Z"g d,Z#dS )7u  Cross-agent file state coordination.

Prevents mangled edits when concurrent subagents (same process, same
filesystem) touch the same file. Complements the single-agent path-overlap
check in ``run_agent._should_parallelize_tool_batch`` — this module catches
the case where subagent B writes a file that subagent A already read, so
A's next write would overwrite B's changes with stale content.

Design
------
A process-wide singleton ``FileStateRegistry`` tracks, per resolved path:

  * per-agent read stamps: {task_id: {path: (mtime, read_ts, partial)}}
  * last writer globally: {path: (task_id, write_ts)}
  * per-path ``threading.Lock`` for read→modify→write critical sections

Three public hooks are used by the file tools:

  * ``record_read(task_id, path, *, partial)`` — called by read_file
  * ``note_write(task_id, path)`` — called after write_file / patch
  * ``check_stale(task_id, path)`` — called BEFORE write_file / patch

Plus ``lock_path(path)`` — a context-manager returning a per-path lock to
wrap the whole read→modify→write block. And ``writes_since(task_id,
since_ts, paths)`` for the subagent-completion reminder in delegate_tool.

All methods are no-ops when ``HERMES_DISABLE_FILE_STATE_GUARD=1`` is set.

This module is intentionally separate from ``_read_tracker`` in
``file_tools.py`` — that tracker is per-task and handles consecutive-read
loop detection, which is a different concern.
    )annotationsN)defaultdict)contextmanager)Path)DictIterableListOptionalTuplei   c                  x    e Zd ZdZd!dZd"dZed#d	            Zd
ddd$dZddd%dZ	d&dZ
d'dZd(dZd!d ZdS ))FileStateRegistryz4Process-wide coordinator for cross-agent file edits.returnNonec                    t          t                    | _        i | _        i | _        t          j                    | _        t          j                    | _        d S N)	r   dict_reads_last_writer_path_locks	threadingLock
_meta_lock_state_lockselfs    5/home/ubuntu/.hermes/hermes-agent/tools/file_state.py__init__zFileStateRegistry.__init__>   sF    7B47H7H:<68#.**$>++    resolvedstrthreading.Lockc                    | j         5  | j                            |          }|t          j                    }|| j        |<   |cd d d            S # 1 swxY w Y   d S r   )r   r   getr   r   r   r   locks      r   	_lock_forzFileStateRegistry._lock_forF   s    _ 	 	#''11D| ~''-1 *	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s   ;AAAc              #     K   |                      |          }|                                 	 dV  |                                 dS # |                                 w xY w)u   Acquire the per-path lock for a read→modify→write section.

        Same process, same filesystem — threads on the same path serialize.
        Different paths proceed in parallel.
        N)r&   acquirereleaser$   s      r   	lock_pathzFileStateRegistry.lock_pathN   sW       ~~h''	EEELLNNNNNDLLNNNNs   A AFN)partialmtimetask_idr+   boolr,   Optional[float]c               |   t                      rd S |2	 t          j                            |          }n# t          $ r Y d S w xY wt          j                    }| j        5  | j        |         }t          |          |t          |          f||<   t          |t                     d d d            d S # 1 swxY w Y   d S r   )	_disabledospathgetmtimeOSErrortimer   r   floatr.   	_cap_dict_MAX_PATHS_PER_AGENT)r   r-   r   r+   r,   nowagent_readss          r   record_readzFileStateRegistry.record_read]   s
    ;; 	F=((22   ikk 	9 	9+g.K%*5\\3W$FK!k#7888	9 	9 	9 	9 	9 	9 	9 	9 	9 	9 	9 	9 	9 	9 	9 	9 	9 	9s"   4 
AAAB11B58B5)r,   c                  t                      rdS |2	 t          j                            |          }n# t          $ r Y dS w xY wt          j                    }| j        5  ||f| j        |<   t          | j        t                     t          |          |df| j        |         |<   t          | j        |         t                     ddd           dS # 1 swxY w Y   dS )u   Record a successful write.

        Updates the global last-writer map AND this agent's own read stamp
        (a write is an implicit read — the agent now knows the current
        content).
        NF)r1   r2   r3   r4   r5   r6   r   r   r8   _MAX_GLOBAL_WRITERSr7   r   r9   )r   r-   r   r,   r:   s        r   
note_writezFileStateRegistry.note_writer   s?    ;; 	F=((22   ikk 	B 	B+2C.Dh'd')<===.3EllC-GDK *dk'*,@AAA	B 	B 	B 	B 	B 	B 	B 	B 	B 	B 	B 	B 	B 	B 	B 	B 	B 	Bs"   4 
AAA'CCCOptional[str]c           	     @   t                      rdS | j        5  | j                            |i                               |          }| j                            |          }ddd           n# 1 swxY w Y   ||dS 	 t
          j                            |          }n# t          $ r Y dS w xY w|K|\  }}||k    r@|| d|dS |d         }||k    r(| d|dt          |           dt          |           dS ||\  }	}
}||	k    r| dS |r| d	S || d
S dS )u  Return a model-facing warning if this write would be stale.

        Three staleness classes, in order of severity:

          1. Sibling subagent wrote this file after this agent's last read.
          2. External/unknown change (mtime differs from our last read).
          3. Agent never read the file (write-without-read).

        Returns ``None`` when the write is safe.  Does not raise — callers
        decide whether to block or warn.
        Nz" was modified by sibling subagent zg but this agent never read it. Read the file before writing to avoid overwriting the sibling's changes.   z at u%    — after this agent's last read at z". Re-read the file before writing.zs was modified since you last read it on disk (external edit or unrecorded writer). Re-read the file before writing.zi was last read with offset/limit pagination (partial view). Re-read the whole file before overwriting it.zS was not read by this agent. Read the file first so you can write an informed edit.)
r1   r   r   r#   r   r2   r3   r4   r5   _fmt_ts)r   r-   r   stamplast_writercurrent_mtime
writer_tid	writer_tsread_ts
read_mtime_read_tsr+   s               r   check_stalezFileStateRegistry.check_stale   s$    ;; 	4 	: 	:KOOGR0044X>>E+//99K	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: 	: =[04	G,,X66MM 	 	 	44	
 "$/!J	W$$=# 1 1%1 1 1  (w&&# ; ;%; ;-4Y-?-?; ;5<W5E5E; ; ; ,1)J'
** 7 7 7
   & & & = I I I
 ts$   A	A--A14A1?B 
B-,B-exclude_task_idsince_tsr7   pathsIterable[str]Dict[str, List[str]]c                d   t                      ri S t          |          }t          t                    }| j        5  | j                                        D ]5\  }\  }}||k    r||k     r||v r||                             |           6	 ddd           n# 1 swxY w Y   t          |          S )a  Return ``{writer_task_id: [paths]}`` for writes done after
        ``since_ts`` by agents OTHER than ``exclude_task_id``.

        Used by delegate_task to append a "subagent modified files the
        parent previously read" reminder to the delegation result.
        N)	r1   setr   listr   r   itemsappendr   )	r   rM   rN   rO   	paths_setoutprG   tss	            r   writes_sincezFileStateRegistry.writes_since   s    ;; 	IJJ	$/$5$5 	. 	.'+'8'>'>'@'@ . .##J00==	>>
O**1---.	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. 	. Cyys   ABBB	List[str]c                    t                      rg S | j        5  t          | j                            |i                                                     cddd           S # 1 swxY w Y   dS )z6Return the list of resolved paths this agent has read.N)r1   r   rT   r   r#   keys)r   r-   s     r   known_readszFileStateRegistry.known_reads   s    ;; 	I 	= 	=4499;;<<	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	= 	=s   :AA#&A#c                   | j         5  | j                                         | j                                         ddd           n# 1 swxY w Y   | j        5  | j                                         ddd           dS # 1 swxY w Y   dS )z*Reset all state.  Intended for tests only.N)r   r   clearr   r   r   r   s    r   ra   zFileStateRegistry.clear   s    	& 	&K##%%%	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& _ 	% 	%""$$$	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	%s#   3AAAB  BB)r   r   )r   r    r   r!   )r   r    )
r-   r    r   r    r+   r.   r,   r/   r   r   )r-   r    r   r    r,   r/   r   r   )r-   r    r   r    r   r@   )rM   r    rN   r7   rO   rP   r   rQ   r-   r    r   r\   )__name__
__module____qualname____doc__r   r&   r   r*   r<   r?   rL   r[   r_   ra    r   r   r   r   ;   s       >>, , , ,       ^& !%9 9 9 9 9 94 "&B B B B B B8I I I IX   4= = = =% % % % % %r   r   r   c                     t           S r   )	_registryrg   r   r   get_registryrj   	  s    r   r.   c                 n    t           j                            dd                                          dk    S )NHERMES_DISABLE_FILE_STATE_GUARD 1)r2   environr#   striprg   r   r   r1   r1     s*    :>>;R@@FFHHCOOr   rZ   r7   r    c                P    t          j        dt          j        |                     S )Nz%H:%M:%S)r6   strftime	localtime)rZ   s    r   rC   rC     s      =T^B%7%7888r   dr   limitintr   c                    t          |           |z
  }|dk    rdS t          |           }t          |          D ]>}	 |                     t	          |                     &# t
          t          f$ r Y  dS w xY wdS )zDTrim a dict to ``limit`` entries by dropping insertion-order oldest.r   N)leniterrangepopnextStopIterationKeyError)rt   ru   overit_s        r   r8   r8     s    q66E>Dqyy	aB4[[  	EE$r((OOOOx( 	 	 	EEE	 s   "AA54A5Fr+   r-   resolved_or_path
str | Pathr+   c               Z    t                               | t          |          |           d S )Nr   )ri   r<   r    )r-   r   r+   s      r   r<   r<   '  s,    '3'7#8#8'JJJJJr   c                V    t                               | t          |                     d S r   )ri   r?   r    r-   r   s     r   r?   r?   +  s'    #&6"7"788888r   r@   c                R    t                               | t          |                    S r   )ri   rL   r    r   s     r   rL   rL   /  s!      #.>*?*?@@@r   c                P    t                               t          |                     S r   )ri   r*   r    )r   s    r   r*   r*   3  s    s#344555r   rM   rN   rO   Iterable[str | Path]rQ   c                N    t                               | |d |D                       S )Nc                ,    g | ]}t          |          S rg   )r    ).0rY   s     r   
<listcomp>z writes_since.<locals>.<listcomp><  s    =T=T=Tc!ff=T=T=Tr   )ri   r[   )rM   rN   rO   s      r   r[   r[   7  s+    
 !!/8=T=Te=T=T=TUUUr   r\   c                6    t                               |           S r   )ri   r_   )r-   s    r   r_   r_   ?  s      )))r   )r   rj   r<   r?   rL   r*   r[   r_   )r   r   )r   r.   )rZ   r7   r   r    )rt   r   ru   rv   r   r   )r-   r    r   r   r+   r.   r   r   )r-   r    r   r   r   r   )r-   r    r   r   r   r@   )r   r   )rM   r    rN   r7   rO   r   r   rQ   rb   )$rf   
__future__r   r2   r   r6   collectionsr   
contextlibr   pathlibr   typingr   r   r	   r
   r   r7   r.   	ReadStampr9   r>   r   ri   rj   r1   rC   r8   r<   r?   rL   r*   r[   r_   __all__rg   r   r   <module>r      s   @ # " " " " " 				      # # # # # # % % % % % %       8 8 8 8 8 8 8 8 8 8 8 8 8 8 %$%	
    G% G% G% G% G% G% G% G%V 	   P P P P
9 9 9 9    PU K K K K K K9 9 9 9A A A A6 6 6 6V V V V* * * *	 	 	r   