
    Pmj'                    d   U 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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 ddlmZ  ej        e          Z e            dz  Zd	Zd
ZdZdZdZg dZ  e!d e"d edd                              Z#e$e%d<   dZ& ej'        d          Z(de)dee)         fdZ*de)de)dee)         fdZ+de)de
fdZ,de)de)fdZ-dTdee
         de
fd Z.de)de
fd!Z/d	e
d"e)de
fd#Z0d"e)de)fd$Z1d	e
d"e)de
fd%Z2	 dTd	e
de)d&ee
         de3fd'Z4d	e
ddfd(Z5e#ddfd)ee)         d	e
de)d*e$d+eee$                  d&ee
         dee6e)e)f         fd,Z7de
dee
         fd-Z8d	e
de)dee)         fd.Z9d/e
defd0Z:d	e
de)ddfd1Z;d	e
de)ddfd2Z<d	e
dee         fd3Z=de
dee         fd4Z>d5e)de$fd6Z?d5e
de$fd7Z@d8e
de)dee)         fd9ZA G d: d;          ZBdee         d<e)de)fd=ZCd>ZDd	e
d?e)de6fd@ZE	 	 	 dUd/e)dBee$         dCee$         dDe6de6f
dEZFd<e
de6fdFZG	 	 	 	 	 dVdHe$dIe6dJee
         dKe$dLeeH         dee)e$f         fdMZI	 	 	 	 	 dWdHe$dOe$dIe6dJee
         dKe$dee)eJf         fdPZKdTdJee
         defdQZLdTdJee
         dee)e$f         fdRZMdTdJee
         dee)e$f         fdSZNdS )XuV	  
Checkpoint Manager — Transparent filesystem snapshots via a single shared
shadow git store.

Creates automatic snapshots of working directories before file-mutating
operations (``write_file``, ``patch``, ``terminal`` with destructive flags),
triggered once per conversation turn.  Provides rollback to any previous
checkpoint.

This is NOT a tool — the LLM never sees it.  It's transparent infrastructure
controlled by the ``checkpoints`` config flag or ``--checkpoints`` CLI flag.

Storage layout (single shared store, git objects deduplicated across projects)
-----------------------------------------------------------------------------

    ~/.hermes/checkpoints/
        store/                          — single bare-ish git repo
            HEAD, config, objects/      — standard git internals (shared)
            refs/hermes/<hash16>        — per-project branch tip
            indexes/<hash16>            — per-project git index
            projects/<hash16>.json      — {workdir, created_at, last_touch}
            info/exclude                — default excludes (shared)
        .last_prune                     — auto-prune idempotency marker
        legacy-<timestamp>/             — archived pre-v2 per-project shadow
                                          repos (auto-migrated on first init)

Why a single store?
-------------------

The pre-v2 design kept a full shadow repo per working directory.  Each one
re-stored most of the project's files under its own ``objects/`` tree, with
zero sharing across worktrees of the same project.  A single user with a
dozen worktrees of the same repo burned ~40 MB each (~500 MB total) storing
the same blobs over and over.  A single shared store lets git's content-
addressable object DB deduplicate across projects and across turns, so adding
a new worktree costs near-zero.

The shadow store uses ``GIT_DIR`` + ``GIT_WORK_TREE`` + ``GIT_INDEX_FILE``
so no git state leaks into the user's project directory.

Auto-maintenance
----------------

Shadow state accumulates over time.  ``prune_checkpoints`` deletes refs whose
recorded working directory no longer exists (orphan) or whose last touch is
older than ``retention_days`` (stale), then runs ``git gc --prune=now`` to
reclaim object storage.  A size-cap pass drops the oldest checkpoints per
project until total store size is under ``max_total_size_mb``.
    N)Path)get_hermes_home)windows_hide_flags)DictListOptionalSetTuple)env_intcheckpointsstorezrefs/hermesindexesprojectszlegacy-)2znode_modules/zdist/zbuild/ztarget/zout/z.next/z.nuxt/z__pycache__/z*.pycz*.pyoz.cache/z.pytest_cache/z.mypy_cache/z.ruff_cache/z	coverage/z	.coveragez.venv/zvenv/zenv/z.git/z.hg/z.svn/z.worktrees/z*.soz*.dylibz*.dllz*.oz*.az*.jarz*.classz*.exez*.objz*.mp4z*.movz*.mkvz*.webmz*.zipz*.tarz*.tar.gzz*.tgzz*.7zz*.rarz*.isoz.envz.env.*z
.env.localz.env.*.localz	.DS_Storez	Thumbs.dbz*.log
   <   HERMES_CHECKPOINT_TIMEOUT   _GIT_TIMEOUTiP  z^[0-9a-fA-F]{4,64}$commit_hashreturnc                     | r|                                  sdS |                     d          rd| S t                              |           sd| S dS )zValidate a commit hash to prevent git argument injection.

    Returns an error string if invalid, None if valid.
    Values starting with '-' would be interpreted as git flags
    (e.g., '--patch', '-p') instead of revision specifiers.
    zEmpty commit hash-z/Invalid commit hash (must not start with '-'): z4Invalid commit hash (expected 4-64 hex characters): N)strip
startswith_COMMIT_HASH_REmatch)r   s    >/home/thesage/.hermes/hermes-agent/tools/checkpoint_manager.py_validate_commit_hashr      st      #k//11 #""c"" QPPPP  -- VUkUUU4    	file_pathworking_dirc                 "   | r|                                  sdS t          j                            |           rd| S t	          |          }|| z                                  }	 |                    |           n# t          $ r d| cY S w xY wdS )zValidate a file path to prevent path traversal outside the working directory.

    Returns an error string if invalid, None if valid.
    zEmpty file pathz/File path must be relative, got absolute path: z7File path escapes the working directory via traversal: N)r   ospathisabs_normalize_pathresolverelative_to
ValueError)r    r!   abs_workdirresolveds       r   _validate_file_pathr,      s    
  !IOO-- !  	w}}Y ONNNN!+..Ki'0022HW[)))) W W WVVVVVVW4s   $A: :BB
path_valuec                 h    t          |                                                                           S )z;Return a canonical absolute path for checkpoint operations.)r   
expanduserr'   )r-   s    r   r&   r&      s(    
&&((00222r   c                     t          t          |                     }t          j        |                                                                          dd         S )z6Deterministic per-project hash: sha256(abs_path)[:16].N   )strr&   hashlibsha256encode	hexdigest)r!   abs_paths     r   _project_hashr8      sF    ?;//00H>(//++,,6688"==r   basec                 $    | pt           t          z  S )z+Return the single shared shadow store path.)CHECKPOINT_BASE_STORE_DIRNAME)r9   s    r   _store_pathr=      s    #O~55r   c                     t                      S )u  Return the shared store path.

    Retained for backward-compatibility with callers / tests that imported
    this helper.  Under v2 the shadow git storage is shared across all
    projects — per-project isolation lives in refs and indexes, not in
    separate repo directories.
    )r=   )r!   s    r   _shadow_repo_pathr?      s     ==r   dir_hashc                     | t           z  |z  S N)_INDEXES_DIRNAMEr   r@   s     r   _index_pathrE      s    ##h..r   c                     t            d|  S )N/)_REFS_PREFIX)r@   s    r   	_ref_namerI      s    ''X'''r   c                 "    | t           z  | dz  S )Nz.json)_PROJECTS_DIRNAMErD   s     r   _project_meta_pathrL      s    $$('9'9'999r   
index_filec                    t          |          }ddlm}  |dd          }t          |           |d<   t          |          |d<   |                    dd           |                    d	d           |t          |          |d
<   n|                    d
d           t
          j        |d<   t
          j        |d<   d|d<   |S )uJ  Build env dict that redirects git to the shared store.

    The shared store is internal Hermes infrastructure — it must NOT inherit
    the user's global or system git config.  User-level settings like
    ``commit.gpgsign = true``, signing hooks, or credential helpers would
    either break background snapshots or, worse, spawn interactive prompts
    (pinentry GUI windows) mid-session every time a file is written.

    Isolation strategy:
    * ``GIT_CONFIG_GLOBAL=<os.devnull>`` — ignore ``~/.gitconfig`` (git 2.32+).
    * ``GIT_CONFIG_SYSTEM=<os.devnull>`` — ignore ``/etc/gitconfig`` (git 2.32+).
    * ``GIT_CONFIG_NOSYSTEM=1`` — legacy belt-and-suspenders for older git.

    ``index_file``, if given, forces git to use a per-project index under
    ``store/indexes/<hash>`` so projects don't race on a shared index.
    r   build_subprocess_envFscrub_secretsinherit_profile_homeGIT_DIRGIT_WORK_TREEGIT_NAMESPACEN GIT_ALTERNATE_OBJECT_DIRECTORIESGIT_INDEX_FILEGIT_CONFIG_GLOBALGIT_CONFIG_SYSTEM1GIT_CONFIG_NOSYSTEM)r&   tools.environments.localrP   r2   popr#   devnull)r   r!   rM   normalized_working_dirrP   envs         r   _git_envrb      s    * -[99 >=====

U
O
O
OCZZC	N566CGGOT"""GG.555 #J $'''!zC!zC!$CJr   c                    dD ]}| |z  }|                                 sg	 |                    dd           t                              d|           O# t          $ r&}t                              d||           Y d}~zd}~ww xY wdS )a  Recreate refs/ and branches/ dirs that ``git gc`` may have removed.

    ``git gc --prune=now`` on a bare repo with only packed refs can remove
    the empty ``refs/heads/`` directory.  Git 2.34+ requires ``refs/`` (and
    some versions require ``branches/``) to exist even when all refs are
    packed in ``packed-refs``.  Without them, ``git add -A`` returns
    ``fatal: not a git repository`` and all checkpoint operations fail
    silently.
    )z
refs/headsbranchesTparentsexist_okz'Repaired missing %s in checkpoint storez(Cannot create %s in checkpoint store: %sN)existsmkdirloggerdebugOSErrorwarning)r   subdirr$   excs       r   _repair_bare_repo_dirsrp     s     - 	 	v~{{}} 	

4$
777FOOOO   >       			 	s   2A
BA==Bargstimeoutallowed_returncodesc                 $   t          |          }|                                sJd| }t                              dd                    dgt          |           z             |           dd|fS |                                sJd| }t                              dd                    dgt          |           z             |           dd|fS t          |t          |          |          }dgt          |           z   }	|pt                      }	 t          j        |	d	d	d
d||t          |          t          j        t                      
  
        }
|
j        dk    }|
j                                        }|
j                                        }|s>|
j        |vr5t                              dd                    |	          |
j        |           |||fS # t          j        $ r? d| dd                    |	           }t                              |d	           dd|fcY S t&          $ r}t)          |dd          }|dk    r6t                              dd                    |	          d	           Y d}~dS d| }t                              dd                    |	          |d	           dd|fcY d}~S d}~wt*          $ rM}t                              dd                    |	          |d	           ddt          |          fcY d}~S d}~ww xY w)a5  Run a git command against the shared store.  Returns (ok, stdout, stderr).

    ``allowed_returncodes`` suppresses error logging for known/expected non-zero
    exits while preserving the normal ``ok = (returncode == 0)`` contract.
    Example: ``git diff --cached --quiet`` returns 1 when changes exist.
    zworking directory not found: zGit command skipped: %s (%s) gitF z&working directory is not a directory: rM   Tutf-8replace)	capture_outputtextencodingerrorsrr   ra   cwdstdincreationflagsr   z(Git command failed: %s (rc=%d) stderr=%szgit timed out after zs: )exc_infofilenameNzGit executable not found: %s)Frw   zgit not foundz,Git command failed before execution: %s (%s)z#Unexpected git error running %s: %s)r&   rh   rj   errorjoinlistis_dirrb   r2   set
subprocessrunDEVNULLr   
returncodestdoutr   stderrTimeoutExpiredFileNotFoundErrorgetattr	Exception)rq   r   r!   rr   rs   rM   r`   msgra   cmdresultokr   r   ro   missing_targets                   r   _run_gitr   -  s[    -[99!((** F.DFF3SXXugT

>R5S5SUXYYYb#~!((** O7MOO3SXXugT

>R5S5SUXYYYb#~
5#455*
M
M
MC'DJJ
C-6%#	*++$ -..
 
 
 !#$$&&$$&& 	f'/BBBLL:v0&   66!!$   @W@@#@@S4(((b#~    j$77U""LL7#QULVVV------F.DFFCSXXc]]TWbfgggb#~ # # #:CHHSMM3Y]^^^b#c(("""""""#s@   CG AL	L(AJ55:J5/L5LAL
L
Lc                    |                                  sdS t          |           }d}t          t          h}t	          |                                           D ]}|j        }||v s|                    t                    r)|ot          j
        d          }| t           | z  }	 |                    dd           n4# t          $ r'}t                              d|           Y d}~ dS d}~ww xY w||z  }	 t          j        t#          |          t#          |                     # t          $ r&}t                              d||           Y d}~d}~ww xY w|}	|t                              d|           |S )a<  Move pre-v2 per-project shadow repos into a ``legacy-<ts>/`` dir.

    The pre-v2 layout had one shadow git repo per working directory directly
    under ``CHECKPOINT_BASE``.  The v2 layout wants a single ``store/`` dir.
    Rather than delete the old data (users might want to recover), rename
    everything except our own v2 entries into ``legacy-<timestamp>/``.  The
    legacy dir is subject to the same retention sweep and can be manually
    cleared with ``hermes checkpoints clear-legacy``.

    Returns the legacy-archive path, or None if nothing to migrate.
    Nz%Y%m%d-%H%M%STre   z'Could not create legacy archive dir: %sz*Could not archive legacy checkpoint %s: %sz_Migrated pre-v2 checkpoint repos to %s. Clear with `hermes checkpoints clear-legacy` when safe.)rh   r=   r<   _PRUNE_MARKER_NAMEr   iterdirnamer   _LEGACY_PREFIXtimestrftimeri   rl   rj   rm   shutilmover2   info)
r9   r   legacy_rootreservedchildr   stampro   dest_s
             r   _migrate_legacy_storer   u  s    ;;== tE"&K 23Hdllnn%% U Uz8t~>> M/22EN!;E!;!;;K!!$!>>>>   H#NNNtttttt T!	UKE

CII.... 	U 	U 	UNNGPSTTTTTTTT	U 	AF	
 	
 	

 s0   %B==
C.C))C.7/D''
E1EEc                 $   | j         }|                                 sD	 |                    dd           n# t          $ r}d| cY d}~S d}~ww xY wt	          |           | dz                                  rdS |                     dd           | t
          z                      d           | t          z                      d           ddlm}  |d	d	
          }t          j
        |d<   t          j
        |d<   d|d<   dD ]}|                    |d           	 t          j        dddt          |           gdddd|t          t          j        t#                      	  	        }|j        dk    rd|j                                         S n(# t          j        t,          f$ r}d| cY d}~S d}~ww xY wt          |          }t/          g d| |           t/          g d| |           t/          g d| |           t/          g d| |           t/          g d| |           | dz  }	|	                    d           |	dz                      d                    t4                    dz   d           t6                              d |            dS )!zInitialise the shared shadow store if needed.  Returns error or None.

    Also performs one-time migration of pre-v2 per-directory shadow repos
    into ``legacy-<timestamp>/``.
    Tre   z"Could not create checkpoint base: NHEAD)rg   r   rO   FrQ   rY   rZ   r[   r\   )rT   rU   rX   rV   rW   rv   initz--barery   rz   )r{   r|   r}   r~   ra   rr   r   r   zShadow store init failed: )configz
user.emailzhermes@local)r   z	user.namezHermes Checkpoint)r   zcommit.gpgsignfalse)r   ztag.gpgSignr   )r   zgc.auto0r   exclude
r}   z"Initialised checkpoint store at %s)parentrh   ri   rl   r   rC   rK   r]   rP   r#   r_   r^   r   r   r2   r   r   r   r   r   r   r   r   r   
write_textr   DEFAULT_EXCLUDESrj   rk   )
r   r!   r9   ro   rP   init_envkr   cfg_wdinfo_dirs
             r   _init_storer     s$    <D<<>> $	>JJtdJ3333 	> 	> 	>=========	> 	d###   t	KKtK,,,
$$d$333
%%t%444
 >=====##%eTTTH$&JH !$&JH !&)H"#2  Q2FHc%jj1dWY,$,..
 
 
 !!G0C0C0E0EGGG "%'89 2 2 21C111111112 YYF555ufEEE9995&III222E6BBB///???'''777v~HNNDN!!!	%%		"##d*W &    LL5u===4s9   5 
AA	A	AA/F F4%F/)F4/F4workdirc                     	 |                                  si S | j                                        }|j        r|j        si S |j        |j        dS # t
          $ r i cY S w xY w)uL  Record the identity of ``workdir``'s parent while the project is live.

    ``(st_dev, st_ino)`` of the parent directory, captured at a moment when
    the workdir itself is reachable, identifies the *directory* — not just
    the path.  A mount point resolves to the mounted filesystem's root while
    the volume is attached and to the underlying (underlay) directory after
    unmount: same path, different directory, different ``(st_dev, st_ino)``.
    Orphan pruning uses this to distinguish "the project was deleted out of
    the directory we knew" from "a different directory is now visible at
    that path because the volume is detached".

    Returns ``{}`` when the workdir is not currently reachable, when the
    filesystem does not provide a usable directory identity (a zero
    ``st_dev`` or ``st_ino`` — e.g. Windows filesystems without file IDs and
    some network shares), or when the probe fails — callers treat all of
    these as "no evidence recorded" and orphan pruning stays conservative
    for the project (never classified as orphan; retention still applies).
    workdir_parent_devworkdir_parent_ino)rh   r   statst_devst_inorl   )r   sts     r   _volume_evidencer     s    &~~ 	I^  ""y 		 	I"$)"$)
 
 	
    			s   A (A A AAc                 6   t          |          }t          | |          }t          j                    }t          t	          |                    ||d}t          t	          |                    }|r|                    |           |                                r	 t          j	        |
                    d                    }t          |t                    r/|                    d|          |d<   |sdD ]}||v r||         ||<   n# t          t          f$ r Y nw xY w	 |j                            dd           |                    t          j        |          d           d	S # t          $ r'}	t(                              d||	           Y d	}	~	d	S d	}	~	ww xY w)
zDCreate or update ``projects/<hash>.json`` with workdir + timestamps.)r   
created_at
last_touchry   r   r   r   Tre   z'Could not write project metadata %s: %sN)r8   rL   r   r2   r&   r   updaterh   jsonloads	read_text
isinstancedictgetrl   r)   r   ri   r   dumpsrj   rk   )
r   r!   r@   	meta_pathnowmetaevidenceexistingkeyro   s
             r   _register_projectr     s   [))H"5(33I
)++C !=!=>> #38 8D < <==H H 	z)"5"5w"5"G"GHHH(D)) 	6%-\\,%D%D\" 6
  L 6 6(??(0DI$ 	 	 	D	Ptd;;;TZ--@@@@@ P P P>	3OOOOOOOOOPs,   A,D DD AE' '
F1FFc                    t          |          }t          | |          }|                                st          | |           dS 	 t	          j        |                    d                    }n# t          t          f$ r i }Y nw xY wt          |t                    si }t          t          |                    |d<   t          j                    |d<   |                    d|d                    t          t          |                    }|r|                    |           	 |                    t	          j        |          d           dS # t          $ r'}t&                              d||           Y d}~dS d}~ww xY w)z7Update last_touch for a project, preserving created_at.Nry   r   r   r   r   z(Could not update project metadata %s: %s)r8   rL   rh   r   r   r   r   rl   r)   r   r   r2   r&   r   
setdefaultr   r   r   r   rj   rk   )r   r!   r@   r   r   r   ro   s          r   _touch_projectr   +  s   [))H"5(33I %---z)--w-??@@Z    dD!! /+6677DODOOL$|"4555
   < <==H HQTZ--@@@@@ Q Q Q?CPPPPPPPPPQs*   (A0 0BB%)E 
FE<<Fc                 j   | t           z  }|                                sg S g }|                    d          D ]z}|j        }	 t	          j        |                    d                    }n# t          t          f$ r Y Fw xY wt          |t                    s`||d<   |                    |           {|S )z/Return all registered projects under the store.z*.jsonry   r   _hash)rK   rh   globstemr   r   r   rl   r)   r   r   append)r   projects_diroutr   r@   r   s         r   _list_projectsr   H  s    ,,L   	C!&&x00 	 		>	:i1171CCDDDD$ 	 	 	H	$%% 	 W

4Js   (A++A?>A?c                 v   g }|                                  s|S |                                 D ]
}|                                s|j        t          k    s|j                            t                    rH|dz                                   s`d}d}|dz  }|                                 rE	 |                    d                                          }n# t          t          f$ r d}d}Y nw xY w|                    ||t          |          o t          |                                           |d           |S )	at  Return pre-v2 per-project shadow repos still directly under ``base``.

    Pre-v2 layout kept one shadow git repo per working directory directly
    under ``CHECKPOINT_BASE`` (identified by a ``HEAD`` file).  This is the
    single source of truth for that scan so a preview built from it (e.g.
    ``store_status``) always matches what ``prune_checkpoints`` deletes.
    r   NFHERMES_WORKDIRry   r   T)r$   r   rh   marker_unreadable)rh   r   r   r   r<   r   r   r   r   rl   UnicodeDecodeErrorr   boolr   )r9   r   r   r   r   	wd_markers         r   _pre_v2_shadow_reposr   [  sf    C;;== 
  ||~~ 	:''5:+@+@+P+P'&&(( 	!%!,,	 	))#--w-??EEGG/0 ) ) ) $(!!!	)
 	

7mm>W(<(<(>(>!2	
 
 	 	 	 	 Js   +(CC,+C,r$   c                     d}	 t          |                               d          D ]}|dz  }|t          k    r|c S n# t          t          f$ r Y nw xY w|S )z;Quick file count estimate (stops early if over _MAX_FILES).r   *   )r   rglob
_MAX_FILESPermissionErrorrl   )r$   countr   s      r   _dir_file_countr     s    Ed!!#&& 	 	AQJEz!! "	 W%   Ls   7> > AAc                     d}	 |                      d          D ]C}	 |                                r||                                j        z  }4# t          $ r Y @w xY wn# t          $ r Y nw xY w|S )z9Best-effort recursive size in bytes.  Returns 0 on error.r   r   )r   is_filer   st_sizerl   )r$   totalps      r   _dir_size_bytesr     s    EC 	 	A99;; .QVVXX--E   		    Ls4   A 0AA 
AA AA 
A+*A+shadow_repoc                     t          | |          }|r|S t          | |           	 | dz                      t          t	          |                    dz   d           n# t
          $ r Y nw xY wdS )a  Backwards-compatible initialiser.

    In v1 ``shadow_repo`` was a per-project dir; in v2 it's the shared
    ``store/`` path (or a test path that we respect).  We initialise the
    store at ``shadow_repo``, create per-project markers, and return None
    on success.
    r   r   ry   r   N)r   r   r   r2   r&   rl   )r   r!   errs      r   _init_shadow_repor     s     k;
/
/C
 
k;///	'	'33,,--4w 	4 	
 	
 	
 	
    4s   7A 
A+*A+c            	       4   e Zd ZdZ	 	 	 	 d#dededed	efd
Zd$dZd%dededefdZ	dede
e         fdZedededdfd            ZdededefdZdedefdZd&dede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ddfdZdeded eddfd!Zdeddfd"ZdS )'CheckpointManageraB  Manages automatic filesystem checkpoints.

    Designed to be owned by AIAgent.  Call ``new_turn()`` at the start of
    each conversation turn and ``ensure_checkpoint(dir, reason)`` before
    any file-mutating tool call.  The manager deduplicates so at most one
    snapshot is taken per directory per turn.

    Parameters
    ----------
    enabled : bool
        Master switch (from config / CLI flag).
    max_snapshots : int
        Keep at most this many checkpoints per directory.
    max_total_size_mb : int
        Hard ceiling on total store size.  Oldest checkpoints per project
        are dropped when the store exceeds this after a commit.
    max_file_size_mb : int
        Skip adding any single file larger than this to a checkpoint.
        (Implemented via ``.gitignore`` excludes + a post-stage size check.)
    F     r   enabledmax_snapshotsmax_total_size_mbmax_file_size_mbc                    || _         t          dt          |                    | _        t          dt          |                    | _        t          dt          |                    | _        t                      | _        d | _        d S )Nr   r   )	r   maxintr   r  r  r   _checkpointed_dirs_git_available)selfr   r   r  r  s        r   __init__zCheckpointManager.__init__  su      C$6$677!$Q,=(>(>!?!? #As+;'<'< = =,/EE.2r   r   Nc                 8    | j                                          dS )zAReset per-turn dedup.  Call at the start of each agent iteration.N)r  clear)r  s    r   new_turnzCheckpointManager.new_turn  s    %%'''''r   autor!   reasonc                 D   | j         sdS | j        <t          j        d          du| _        | j        st                              d           | j        sdS t          t          |                    }|dt          t          j	                              hv rt                              d|           dS || j
        v rdS | j
                            |           	 |                     ||          S # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)u   Take a checkpoint if enabled and not already done this turn.

        Returns True if a checkpoint was taken, False otherwise.
        Never raises — all errors are silently logged.
        FNrv   z#Checkpoints disabled: git not foundrG   z,Checkpoint skipped: directory too broad (%s)z!Checkpoint failed (non-fatal): %s)r   r  r   whichrj   rk   r2   r&   r   homer  add_taker   )r  r!   r  abs_dires        r   ensure_checkpointz#CheckpointManager.ensure_checkpoint  s5    | 	5&"(,u"5"5T"AD& DBCCC" 	5ok2233 sC	,,---LLGQQQ5d---5##G,,,	::gv... 	 	 	LL<a@@@55555	s   C/ /
D9DDc           	         t          t          |                    }t          t                    }|dz                                  sg S t          t          |                    }t          d|ddt          | j                  g||ddh          \  }}}|r|sg S g }|	                                D ]}	|	
                    dd	          }
t          |
          d
k    r}|
d         |
d         |
d         |
d	         dddd}t          dd|
d          d|
d         g||ddh          \  }}}|r|r|                     ||           |                    |           |S )z?List available checkpoints for a directory (most recent first).r   logz--format=%H|%h|%aI|%sz-n      rs   |      r   r      )hash
short_hash	timestampr  files_changed
insertions	deletionsdiffz--shortstatz~1)r2   r&   r=   r;   rh   rI   r8   r   r   
splitlinessplitlen_parse_shortstatr   )r  r!   r  r   refr   r   r   resultslinepartsentrystat_okstat_outs                 r   list_checkpointsz"CheckpointManager.list_checkpoints  s   ok2233O,,&&(( 	Ig..// C0$D<N8O8OP7!$c

 
 
FA  	 	I %%'' 	& 	&DJJsA&&E5zzQ!!H"'(!&q#Ah%&"#!"  (0]uQxOOOU1XF7),c
( ( ($1
  ;x ;))(E:::u%%%r   	stat_liner/  c                 r   t          j        d|           }|r%t          |                    d                    |d<   t          j        d|           }|r%t          |                    d                    |d<   t          j        d|           }|r't          |                    d                    |d<   dS dS )	z-Parse git --shortstat output into entry dict.z
(\d+) filer   r#  z(\d+) insertionr$  z(\d+) deletionr%  N)researchr  group)r3  r/  ms      r   r*  z"CheckpointManager._parse_shortstat8  s     ImY// 	5%(__E/"I()44 	2"%aggajj//E,I'33 	1!$QWWQZZE+	1 	1r   r   c                 l   t          |          }|rd|dS t          t          |                    }t          t                    }|dz                                  sdddS t          dd|g||          \  }}}|s	dd| ddS t          |          }	t          ||	          }
t          d	d
g||t          dz  |
           t          dd|dg|||
          \  }}}t          d|ddg|||
          \  }}}t          |	          }t          d|g|||
dh           |s|sdddS d|r|nd|r|nddS )z<Show diff between a checkpoint and the current working tree.Fsuccessr   r   'No checkpoints exist for this directorycat-file-tCheckpoint '' not foundr  -Ar  rr   rM   r&  z--stat--cachedrx   z
--no-color	read-treer  rM   rs   zCould not generate diffTrw   )r;  r   r&  )r   r2   r&   r=   r;   rh   r   r8   rE   r   rI   )r  r!   r   hash_errr  r   r   r   r   r@   rM   ok_statr1  ok_diffdiff_outr+  s                   r   r&  zCheckpointManager.diffE  s   (55 	9$x888ok2233O,,&&(( 	Z$/XYYY{+UG
 

As  	X$/Vk/V/V/VWWW )) 11
 	%w%)j	B 	B 	B 	B  (X{J77z 
  
  
1  ([*l;7z 
  
  
1 !!+s#UG&&)U	, 	, 	, 	,  	Jw 	J$/HIII  '/HHR '/HHR
 
 	
r   c                 \   |                      |          }|sdddddS |d                             d          pd}|                     ||          }|                    d          rE|                    d|           |                    d          s|                    d	          sd|d
<   |S )uN  Show the cumulative diff of everything changed in this directory.

        This powers ``/diff session``.  It answers "what has Hermes changed
        here?" by diffing the *earliest retained checkpoint* — the snapshot
        taken before the first recorded edit — against the current working
        tree.  Because checkpoints are captured just before each file-mutating
        tool call, that baseline is the pre-edit state, so the diff covers the
        first edit and everything after it.

        Note: checkpoints are a persistent per-project ref, so the earliest
        *retained* checkpoint may predate the current session (or, after
        pruning, postdate its true start).  It is an approximation of "what
        Hermes changed", not an exact per-session ledger.

        Returns the same shape as :meth:`diff` (``{"success", "stat",
        "diff"}``).  When no checkpoints exist yet — nothing has been edited —
        the call still *succeeds* with empty output and ``"empty": True`` so
        callers can show a friendly "no changes" message rather than an error.
        Trw   )r;  r   r&  emptyr   r;  baseliner   r&  rK  )r2  r   r&  r   )r  r!   r   rM  r   s        r   session_diffzCheckpointManager.session_diffw  s    ( ++K88 	L#RdKKKr?&&v..4";11::i   	'j(333::f%% 'fjj.@.@ '"&wr   r    c                    t          |          }|rd|dS t          t          |                    }|rt          ||          }|rd|dS t	          t
                    }|dz                                  sdddS t          dd|g||          \  }}	}
|sdd| d|
pd	d
S |                     |d|d	d          d           t          |          }t          ||          }|r|nd}t          d|d|g||t          dz  |          \  }}}
|sdd|
 |
pd	d
S t          ddd|g||          \  }}}	|r|nd}d|d	d         ||d}|r||d<   |S )z$Restore files to a checkpoint state.Fr:  r   r<  r=  r>  r?  r@  N)r;  r   rk   z$pre-rollback snapshot (restoring to    ).checkout--r  rB  zRestore failed: r  --format=%s-1unknownT)r;  restored_tor  	directoryfile)r   r2   r&   r,   r=   r;   rh   r   r  r8   rE   r   )r  r!   r   r    rF  r  path_errr   r   r   r   r@   rM   restore_targetr   ok2
reason_outr  r   s                      r   restorezCheckpointManager.restore  s   (55 	9$x888ok2233 	=*9g>>H =#(8<<<O,,&&(( 	Z$/XYYY{+UG
 

As  	*$/Vk/V/V/V [D* * * 	

7U;rPQr?UUUVVV )) 11
&/8S"dN;7L1$4!
 
 
FC  	*$/G#/G/G [D* * * &M45ug
 
Z  #1	 &rr? 	
 
  	'&F6Nr   c                    t          |          }|                                r|}n|j        }h d}|j        k    r<t          fd|D                       rt	                    S j        j        k    <t	          |          S )z?Resolve a file path to its working directory for checkpointing.>	   .hg.gitgo.modpom.xml
Cargo.tomlpackage.jsonpyproject.tomlGemfileMakefilec              3   F   K   | ]}|z                                   V  d S rB   )rh   ).0r8  checks     r   	<genexpr>z=CheckpointManager.get_working_dir_for_path.<locals>.<genexpr>  s3      99AEAI%%''999999r   )r&   r   r   anyr2   )r  r    r$   	candidatemarkersrl  s        @r   get_working_dir_for_pathz*CheckpointManager.get_working_dir_for_path  s    y));;== 	$IIIG G Gu|##999999999 "5zz!LE u|##
 9~~r   c                    t          t                    }t          ||          }|rt                              d|           dS t          ||           t          |          t          k    r#t                              dt          |           dS t          |          }t          ||          }t          |          }|                                r`t          dd|dz   g||dh          \  }}	}
|r|	rt          d	|	g|||dh
           nB	 |                                 n,# t          $ r Y n w xY w|j                            dd           t          ddg||t"          dz  |          \  }}
}|st                              d|           dS | j        dk    r|                     |||           t          dd|dz   g||dh          \  }}	}
|ot)          |	          }|r=t          ddd|	g||dh|          \  }}
}
|rt                              d|           dS nLt          ddg|||          \  }}}
|r1|                                st                              d|           dS t          dg|||          \  }}}|r|st                              d|           dS d|d|d g}|r	d|d!|	d|d g}t          ||||          \  }}}|r|st                              d"|           dS d#||g}|rd#|||	g}t          |||          \  }}
}|st                              d$|           dS t                              d%|||d&d'                    |                     |||           |                     |           dS )(z*Take a snapshot.  Returns True on success.z Checkpoint store init failed: %sFz#Checkpoint skipped: >%d files in %s	rev-parsez--verifyz	^{commit}r  r  rD  rE  Tre   r  rA  r  rB  zCheckpoint git-add failed: %sr   z
diff-indexrC  --quietr   )rs   rM   z$Checkpoint skipped: no changes in %sls-filesrx   z$Checkpoint skipped: empty tree in %sz
write-treez Checkpoint write-tree failed: %scommit-tree-m--no-gpg-sign-pz!Checkpoint commit-tree failed: %s
update-refz Checkpoint update-ref failed: %szCheckpoint taken in %s: %s (%s)NrP  )r=   r;   r   rj   rk   r   r   r   r8   rE   rI   rh   r   unlinkrl   r   ri   r   r  _drop_oversize_from_indexr   r   _prune_enforce_size_cap)r  r!   r  r   r   r@   rM   r+  ok_ref
ref_commitr   r   has_refrH  ok_lsls_outok_treetree_shacommit_args	ok_commitnew_shaupdate_args	ok_updates                          r   r  zCheckpointManager._take  s   O,,%-- 	LL;SAAA5uk*** ;''*44LL>
KXXX5 -- 11
!!
  	A$,j#*;<{%(E% % %!FJ
  *  *-;)),	    %%''''   D ##D4#@@@ DM5+ 1$
 
 

As  	LL8#>>>5 1$$**5+zJJJ
 !)*cK&78;!$!
 !
 !

A
 -T*-- 	$z9jA{%&C%	  MGQ  C[QQQu
  (Z({%     E61
  V\\^^ C[QQQu "*NE;!"
 "
 "
3  	h 	LL;SAAA5 %hfoN 	e((D*dFTcdK"*!#
 #
 #
	7C  	 	LL<cBBB5 $S'2 	C'gzBK$
 
	1c  	LL;SAAA56VWUWVWUW[YYY 	E;,,, 	u%%%ts   D! !
D.-D.r   rM   c                 b   | j         dz  dz  }|dk    rdS t          g d|||          \  }}}|r|sdS d |                    d          D             }t          |          }	g }
|D ]K}	 |	|z                                  j        }n# t          $ r Y ,w xY w||k    r|
                    |           L|
sdS t          	                    dt          |
          | j                    d	}t          dt          |
          |          D ])}|
|||z            }t          g d
|z   |||dh           *dS )zRemove any staged file larger than ``max_file_size_mb`` from the index.

        Lets the agent keep snapshotting source code while refusing to
        swallow generated assets (datasets, model weights, logs, videos).
           r   N)ru  rC  z-zrx   c                     g | ]}||S  r  )rk  r   s     r   
<listcomp>z?CheckpointManager._drop_oversize_from_index.<locals>.<listcomp>  s    666qA6666r    z<Checkpoint: dropping %d oversize file(s) (>%d MB) from index   )rmrC  rt  rT  r  rE  )r  r   r(  r&   r   r   rl   r   rj   rk   r)  range)r  r   r!   rM   capr   r   r   pathsr*   oversizerelsizeBATCHichunks                   r   r|  z+CheckpointManager._drop_oversize_from_indexl  s    #d*T1!88F ***;:
 
 
FA  	 	F 76FLL00666%k22  	% 	%C#c)//119   czz$$$ 	FJMM40	
 	
 	
 q#h--// 	 	AQq5y[)E333e;{z%(E    	 	s   *B
BBr+  c                    t          dd|g||dh          \  }}}|sdS 	 t          |          }n# t          $ r Y dS w xY w|| j        k    rdS t          dd|g||          \  }}	}|r|	sdS |	                                }
|
| j         d         }d}|D ]t}t          d| dg||          \  }}}|r|s dS t          d	d
d|g||          \  }}}|r|r|nd}d|d|dg}|	d|d|d|dg}t          |||          \  }}}|r|s dS |}u|dS t          d||g||           t          g d||           t          g d||t
          dz             t          |           dS )u  Keep only the last ``max_snapshots`` commits on the per-project ref.

        v1's ``_prune`` was documented as a no-op (``git``'s pack mechanism
        was supposed to handle it, but only the log view was limited — loose
        objects accumulated forever).  v2 actually rewrites the ref to drop
        commits older than ``max_snapshots`` and then runs ``git gc`` on the
        store so unreachable objects are reclaimed.
        rev-list--countr  r  N	--reversers  ^{tree}r  rU  rV  
checkpointrv  rw  rx  ry  rz  reflogexpirez--expire=nowz--allgcz--prune=nowrt  r  rr   )r   r  r)   r   r'  r   rp   )r  r   r!   r+  r   r   r   r   ok_listlist_outcommitskeep
new_parentshar  r  ok_msgr   
commit_msgrq   r  r  s                         r   r}  zCheckpointManager._prune  si    !C(%!$
 
 
FA  	F	KKEE 	 	 	FF	D&&&F  (c*E; 
  
1  	h 	F%%''**++, %)
 	! 	!C#+///0%$ $ GXq  ( %tS15+ NFC !'@3@LJ!8T:OD%%xzj/;$,T5+$F$F!Iw G  JJF,Z0%EEE 	999;	
 	
 	
 	,,,;q(8	
 	
 	
 	
 	u%%%%%s   1 
??c           
         | j         dk    rdS | j         dz  dz  }t          |          }||k    rdS t                              d| j         |dz             t	          ddt
          g|t          |j                  dh	          \  }}}|r|sdS d
 |                                D             }d}t          d          D ]}t          |          }||k    r n|D ]}	t	          dd|	g|t          |j                  dh	          \  }
}}	 |
rt          |          nd}n# t          $ r d}Y nw xY w|dk    r^t	          dd|	g|t          |j                            \  }}}|r|s|                                }|dd         }d}d}|D ]}t	          d| dg|t          |j                            \  }}}|r|sd} nwt	          ddd|g|t          |j                            \  }}}|r|r|nd}d|d|dg}|	d|d|d|dg}t	          ||t          |j                            \  }}}|r|sd} n|}|s|dt	          d|	|g|t          |j                             d}|s nt	          g d|t          |j                             t	          g d|t          |j                  t          dz              t          |           dS )!zIf total store size exceeds ``max_total_size_mb``, drop oldest
        checkpoints across ALL projects until under the cap.
        r   Nr  uA   Checkpoint store exceeded %d MB (actual %d MB) — pruning oldest   for-each-ref--format=%(refname)r  r  c                 :    g | ]}|                                 |S r  r   rk  rs     r   r  z7CheckpointManager._enforce_size_cap.<locals>.<listcomp>  s%    <<<a!''))<<<<r   Fr   r  r  r   r  rs  r  Tr  rU  rV  r  rv  rw  rx  ry  rz  r  r  r  r  )r  r   rj   r   r   rH   r2   r   r'  r  r  r)   r   rp   )r  r   	cap_bytesr  r   r   r   refsany_droppedr+  ok_count	count_outr   r  r  r  r  r  failr  r  r  r  r   r  rq   r  r  s                               r   r~  z#CheckpointManager._enforce_size_cap  s    !Q&&F*T1D8	u%%9FO"D[$9	
 	
 	
 !2LA3u|$$!$
 
 
FA
  	 	F<<6,,..<<<r 1	 1	A"5))Dy   +# +#)1C0%U\9J9J),* * *&)Q.6=C	NNNAEE!   EEEA::'/c2E3u|;L;L( ($1  h "--//qrr{,0
 ) )C+3$&7&7&78%U\ARAR, ,(GXq # ( #%-tS95#elBSBS& &NFC )/!H3!HLJ)8T:WD!- -xz $j/ C,4T5#elBSBS,T,T)Iw$ G #!(JJ :-,Z8%U\ARARSSS"  	9993u|$$	
 	
 	
 	,,,3u|$$lQ.>	
 	
 	
 	
 	u%%%%%s   DD.-D.)Fr   r   r   )r   N)r  rB   )__name__
__module____qualname____doc__r   r  r	  r  r2   r  r   r   r2  staticmethodr*  r&  rN  r_  rq  r  r   r|  r}  r~  r  r   r   r   r     sy        . !$ "3 33 3 	3
 3 3 3 3$( ( ( (   S  #  4        D'C 'DJ ' ' ' 'R 
1C 
1 
1 
1 
1 
1 \
10
 0
# 0
$ 0
 0
 0
 0
d     @7 73 7S 7S 7TX 7 7 7 7r# #    ,D Dc Dd D D D DL,,(+,9=,	, , , ,\A&D A&s A& A& A& A& A& A&FV&t V& V& V& V& V& V& V&r   r   rY  c                 "   | sd| S d| dg}t          | d          D ]\  }}|d         }d|v r}|                    d          d                             d          d                             d	          d         d
d         }|d                             d          d         }| d| }|                    dd          }|                    dd          }|                    dd          }	|rd| d|dk    rdnd d| d|	 d	}
nd}
|                    d| d|d          d| d|d          |
 	           |                    d           |                    d           |                    d           d                    |          S )z+Format checkpoint list for display to user.zNo checkpoints found for u   📸 Checkpoints for z:
r   r"  T+r   r   N   ru   r#  r$  r%  z  (z filesrw   z, +z/-rQ  z  z. r!  r  z4
  /rollback <N>             restore to checkpoint Nz>  /rollback diff <N>        preview changes since checkpoint NzC  /rollback <N> <file>      restore a single file from checkpoint Nr   )	enumerater(  r   r   r   )r   rY  linesr  cptsdatefilesinsdeler   s              r   format_checkpoint_listr  6  s    7696663Y3334E;** O O2_"99#q!'',,Q/55c::1=bqbABk?((--a0D2B**ff\1%%vvk1%% 	QQQEQJJSSBQQ3QQ$QQQDDDM!MMr,/MM2MMHMtMMNNNN	LLHIII	LLQRRR	LLVWWW99Ur   z.last_pruner+  c                 `    t          dd|g| t          | j                  dh          \  }}}|S )z6Delete a ref from the store.  Returns True on success.rz  z-dr  r  )r   r2   r   )r   r+  r   r   s       r   _delete_refr  ^  s@    	tS!5#el*;*; E  HB1 Ir   T
parent_dev
parent_inorequire_parent_identityc                    | sdS t          |           }	 |                                rdS |j        }||k    rdS |                                sdS |-|+|                                }|j        |j        f||fk    rdS n|rdS t          |          rdS t          j	        
                    |          S # t          $ r Y dS w xY w)us
  True only when we can positively observe that ``workdir`` was removed.

    ``Path.exists()`` returns False for a deleted directory AND for one whose
    storage simply is not attached right now — an unplugged external drive, a
    network share behind a downed VPN, a bind-mount absent from this
    container, an offline Windows mapped drive. Orphan pruning deletes the
    project's entire checkpoint history, so treating that ambiguity as
    "deleted" throws away the user's restore points over a transient mount
    state, unattended, at startup.

    Require corroboration, in three steps.

    First, the parent directory must be present, so the absence of the project
    inside it is something we actually observed. When the parent is missing
    too, the volume is not there and we know nothing.

    Second, the present parent must be the directory we knew — not merely a
    directory at the same path. Unmounting swaps the directory visible at a
    mount point: while the volume is attached the path resolves to the
    mounted filesystem's root; after detach it resolves to the *underlying*
    (underlay) directory, which may carry entries of its own (a ``.keep``
    placeholder, sibling mount points). Those entries were never next to the
    project and prove nothing about the volume being attached. So the
    parent's ``(st_dev, st_ino)`` must match the identity recorded in the
    project's metadata while the project was observably live
    (``parent_dev``/``parent_ino``). A mismatch means a different directory
    is visible at that path — a detached volume, not an observed deletion.
    When no identity was ever recorded (metadata written by an older
    version) and ``require_parent_identity`` is True, stay conservative and
    do not classify as orphan. Callers that have no identity channel at all
    (the frozen pre-v2 layout) pass ``require_parent_identity=False`` to
    keep the structural checks only.

    Third, the (identity-confirmed) parent must actually carry information.
    Unmounting leaves classic static mount points (``/mnt/volume/proj``, an
    fstab entry, a container bind-mount) behind as *empty* directories, so an
    empty parent is the signature of a detached volume just as much as of a
    deleted project. Prune only when the parent holds something else (we
    observed a populated directory that does not contain the project) or is
    itself a live mount point (the volume is demonstrably attached and the
    project is demonstrably not on it).

    Genuinely abandoned projects are still reclaimed by the retention/stale
    rule, which runs off ``last_touch`` rather than a filesystem probe.
    FNT)r   rh   r   r   r   r   r   _dir_has_any_entryr#   r$   ismountrl   )r   r  r  r  r$   r   r   s          r   _workdir_is_observably_goner  g  s    f  u==D;;== 	5 T>>5}} 	5!j&<B	29%*j)AAA u	 B
 % 	 5f%% 	4
 wv&&&   uus3   B3 B3 B3 ,B3 >B3 B3 B3 3
C Cc                     t          j        |           5 }|D ]} ddd           dS 	 ddd           n# 1 swxY w Y   dS )zTrue when ``directory`` contains at least one entry.

    Stops after the first entry rather than materializing the listing; a
    project root can hold a large tree.
    NTF)r#   scandir)rY  entriesr   s      r   r  r    s     
I		 ' 	 	A       	               5s   599   retention_daysdelete_orphanscheckpoint_baser  orphan_allowlistc           
      L   |pt           }dddddd}|                                s|S t          |          }d}| dk    rt          j                    | dz  z
  }|                                D ]
}	|	                                s|	j        t          k    r)|	j                            t                    r| dk    rO	 |	
                                j        }
n# t          $ r Y vw xY w|
|k    r	 t          |	          }t          j        |	           |dxx         |z  cc<   |dxx         dz  cc<   # t          $ r7}|dxx         dz  cc<   t                              d	|	|           Y d
}~d
}~ww xY wt#          |          D ]~}|d         }	|dxx         dz  cc<   d
}|r<|d         s4|d         t%          |d         d          r|t'          |	          |v rd}|z| dk    rtd}	 |	                    d          D ]<}	 |
                                j        }t+          ||          }-# t          $ r Y 9w xY wn# t          $ r Y nw xY w|dk    r||k     rd}|	 t          |	          }t          j        |	           |dxx         |z  cc<   |dk    r|dxx         dz  cc<   n|dxx         dz  cc<   7# t          $ r<}|dxx         dz  cc<   t                              d|	j        |           Y d
}~xd
}~ww xY wt-          |          }|dz                                  rt/          |          D ]}|                    d          pd}|                    d          pd}|s4|dxx         dz  cc<   d
}|                    d          }|                    d          }t3          |t4                    rt3          |t6                    rd
}t3          |t4                    rt3          |t6                    rd
}|r|rt%          |||          r	|||v rd}n9| dk    r3t9          |                    dd          pd          }|dk    r||k     rd}|$t;          |          }t=          ||           	 t?          ||          }|                                r|                                  n# t          $ r Y nw xY w	 tC          ||          }|                                r|                                  n# t          $ r Y nw xY w|dk    r|dxx         dz  cc<   |dxx         dz  cc<   tE          g d|t'          |                     tE          g d|t'          |          tF          dz              tI          |           |dk    rY|d!z  d!z  }tK          d"          D ]}t          |          }||k    r ntE          d#d$tL          g|t'          |          d%h&          \  }}} |rd' |'                                D             ng }!|!s nzd}"|!D ]n}tE          d(d)|g|t'          |          d%h&          \  }#}$} 	 |#rt5          |$          nd}%n# tP          $ r d}%Y nw xY w|%dk    rYtE          d(d*|g|t'          |                    \  }&}'} |&r|'s|''                                }(|(dd
         })d
}*d}+|)D ]},tE          d+|, d,g|t'          |                    \  }-}.} |-r|.sd-}+ nmtE          d.d/d0|,g|t'          |                    \  }/}
} |/r|
r|
nd1}0d2|.d3|0d4g}1|*	d2|.d5|*d3|0d4g}1tE          |1|t'          |                    \  }2}3} |2r|3sd-}+ n|3}*|+s|*KtE          d6||*g|t'          |                     d-}"p|"s ntE          g d|t'          |                     tE          g d|t'          |          tF          dz              tI          |           t          |          }4||4z
  }5t+          |d         |5          |d<   |S )7u  Delete stale/orphan checkpoints and reclaim store space.

    A project entry is deleted when either:

    * ``delete_orphans=True`` and its ``workdir`` no longer exists on disk
      (the original project was deleted / moved); OR
    * its ``last_touch`` is older than ``retention_days`` days.

    ``orphan_allowlist``, when not ``None``, restricts orphan deletion to
    the given identities (v2 project ``_hash`` strings and/or pre-v2 shadow
    repo paths as ``str``). This lets a caller that showed the user a
    confirmation preview (built from ``store_status()``) bind the resulting
    deletion to exactly what was displayed — a project that only becomes
    orphaned *after* the preview (e.g. its workdir vanishes while the human
    is answering the prompt) is skipped rather than swept up under the
    earlier confirmation. Pass ``None`` (the default) to delete every
    currently-orphaned project, e.g. for ``--force`` or unattended callers
    that never show a preview.

    Additionally, if ``max_total_size_mb > 0`` and the store exceeds that
    after orphan/stale pruning, the oldest commit per remaining project is
    dropped until the store is under the cap.

    Legacy-archive dirs (``legacy-*``) older than ``retention_days`` are
    also deleted.

    Returns a dict with counts ``{"scanned", "deleted_orphan",
    "deleted_stale", "errors", "bytes_freed"}``.

    Never raises — maintenance must never block interactive startup.
    r   scanneddeleted_orphandeleted_staler~   bytes_freedg        iQ r  r  r   r~   z&Failed to delete legacy archive %s: %sNr$   r  r   r   F)r  orphanr   staler  z&Failed to prune checkpoint repo %s: %sr   r   rw   r   r   )r  r  r   r  r  r  r  r  r   r  r  r  r  c                 :    g | ]}|                                 |S r  r  r  s     r   r  z%prune_checkpoints.<locals>.<listcomp>  s%    DDDa!''))DDDDr   r  r  r  rs  r  Tr  rU  rV  r  rv  rw  rx  ry  rz  ))r;   rh   r   r   r   r   r   r<   r   r   r   st_mtimerl   r   rmtreerj   rm   r   r  r2   r   r  r=   r   r   r   r  r   floatrI   r  rE   r{  rL   r   r   rp   r  rH   r'  r)   )6r  r  r  r  r  r9   r   size_beforecutoffr   r8  r  ro   repor  newestr   mtr   r   r@   r   r  r  r   r+  idxmpr  _ir   r   r   r  any_dropok_cr  r   ok_llor  r  r  r  r  ok_ttshaok_mr   rq   ok_cmr  
size_afterdeltas6                                                         r   prune_checkpointsr     s
   L -oD F ;;== !$''K F~55 U U||~~ 	:'':  00 	U""JJLL)   F{{U&u--e$$$}%%%-%%%'''1,'''' U U Ux   A%   GPSTTTTTTTTU	U, %T** ,V ,VVyQ $	,-	 Y' /OU  	 ( ")SZZ;K-K-KF>nq00FS)) ! !A!VVXX.!$VR" ! ! ! !	!    zzfvoo >
	V"5))DM%   =!!!T)!!!!!'(((A-(((('''1,''' 	V 	V 	V8!NNCUZQTUUUUUUUU	V
 E   F*"5)) 2	- 2	-Dxx((.BHhhy))/RG 9"F"677J"677Jj#.. "*Z2N2N "!
j#.. "*Z2N2N "!
%  % 3#-#-  	% &-=M1M1M!!##"488L!#<#<#ABB
>>j6&9&9$F~H%%Cs###!%22::<< !JJLLL   'x8899;;  IIKKK   !!'(((A-(((('''1,'''' 	9993t99	
 	
 	
 	,,,3t99lQ&6	
 	
 	
 	
 	u%%% q  )D047IBii : :&u--9$$E (#%:LI3t99),! ! !FA
 IKRDD6#4#4#6#6DDDDPR E  +$ +$C)1#Y4eSYY-0E* * *&D)Q"26 =IA% " " " !"zz "*#[#6s4yy# #KD"a   !r !  mmooG"122;D04J D# - -(0(S*;*;*;<eSYY) )dA  $ "4 "#'D!E%-"M4=uc$ii& &
a $(?A?aa< -tT3P%1$14z$(#$@D,4T5#d)),L,L)w$ "G "#'D!E%,

 !z1 lC<eSYYOOO#HH E===s4yy   000s4yy,*:    #5))) &&J*$E} 5u==F=Ms   <C
C#"C#.AD22
E3<,E..E3-I )H/.I /
H<9I ;H<<I  
II"AJ>>
L1K??L8R::
SS8T
TTY((Y76Y7   min_interval_hoursc                    |pt           }ddi}	 |                                sdddddd|d<   |S |t          z  }t          j                    }|                                ra	 t	          |                    d                                                    }	||	z
  |dz  k     rd	|d<   |S n# t          t          f$ r Y nw xY wt          | |||
          }
|
|d<   	 |
                    t          |          d           n2# t          $ r%}t                              d|           Y d}~nd}~ww xY w|
d         |
d         z   }|dk    r3t                              d||
d         |
d         |
d         dz             nD# t          $ r7}t                              d|           t          |          |d<   Y d}~nd}~ww xY w|S )a'  Idempotent wrapper around ``prune_checkpoints`` for startup hooks.

    Writes ``CHECKPOINT_BASE/.last_prune`` on completion so subsequent
    calls within ``min_interval_hours`` short-circuit.

    Returns ``{"skipped": bool, "result": prune_checkpoints-dict,
    "error": optional str}``.
    skippedFr   r  r   ry   r   i  T)r  r  r  r  z+Could not write checkpoint prune marker: %sNr  r  zZcheckpoint auto-maintenance: pruned %d entry(ies) (%d orphan, %d stale), reclaimed %.1f MBr  r  z&checkpoint auto-maintenance failed: %sr   )r;   rh   r   r   r  r   r   rl   r)   r   r   r2   rj   rk   r   r   rm   )r  r  r  r  r  r9   r   markerr   last_tsr   ro   r   s                r   maybe_auto_prune_checkpointsr    sC    -oD'/C, {{}} 	AA CM J**ikk==?? 	 0 0' 0 B B H H J JKK=#5#<<<%)C	NJ = Z(    #)) /	
 
 
 H	Mc#hh9999 	M 	M 	MLLFLLLLLLLL	M '(6/+BB199KK;'('}%5         ?EEE3xxG  Jsl    E= 1E= "AB+ *E= +B?<E= >B??E= $D  ?E=  
D/
D*%E= *D//AE= =
F>-F99F>c           
      d   | pt           }t          |          ddddg g g d}|                                s|S t          |          }|                                r@t	          |          |d<   |dz                                  rt          |          D ]}|                    d          pd}|                    d          pd}t          |          }t          dd	|g|t          |          d
h          \  }}	}
	 |rt          |	          nd}n# t          $ r d}Y nw xY w|d                             ||t          |          o t          |                                          |                    d          |                    d          |d           t          |d                   |d<   d t          |          D             |d<   |                                D ]}|                                r|j                            t(                    r	 t	          |          }n# t*          $ r d}Y nw xY w|dxx         |z  cc<   	 |                                j        }n# t*          $ r d}Y nw xY w|d                             |j        ||d           t	          |          |d<   |S )un  Return a summary of the shadow store.

    ``{"base": path, "store_size_bytes": N, "legacy_size_bytes": N,
       "total_size_bytes": N, "project_count": N, "projects": [...],
       "pre_v2_projects": [...], "legacy_archives": [...]}``

    ``pre_v2_projects`` covers shadow repos still on the pre-v2 per-project
    layout (``base/<hash>/HEAD``) — distinct from ``legacy_archives``, which
    are already-migrated ``legacy-<ts>/`` dirs. Callers that preview an
    orphan-deletion sweep must include both ``projects`` and
    ``pre_v2_projects``, since ``prune_checkpoints`` deletes orphans from
    both layouts.
    r   )r9   store_size_byteslegacy_size_bytestotal_size_bytesproject_countr   pre_v2_projectslegacy_archivesr	  r   r   rw   r   r  r  r  r  r   r   r   )r   r   rh   r   r   r  r  c                 X    g | ]'}t          |d                    |d         |d         d(S )r$   r   rh   )r$   r   rh   )r2   r  s     r   r  z store_status.<locals>.<listcomp>]  sL        	 &	NN|k	
 	
  r   r  r
  r  )r   
size_bytesmtimer  )r;   r2   rh   r=   r   r   r   rI   r   r  r)   r   r   r   r)  r   r   r   r   r   r   rl   r   r  )r  r9   r   r   r   r@   r   r+  r   r  r   r  r   r  r  s                  r   store_statusr  '  s0    -oDD			 	C ;;== 
E||~~ "1%"8"8FN""$$ 	&u--  88G,,2((9--3))#+C0%T),$ $ $ Iq 029c)nnnGG!      GGG J&&$&"7mmFW0D0D0F0F"&((<"8"8"&((<"8"8&( (     s://C  &d++  C   <<>> 	ej33NCC 	&u--   #$$$,$$$ZZ\\*   !"))
"+ +    .d33CJs6   >DD! D!H**H98H9I''I65I6c                    | pt           }ddd}|                                s|S t          |          }	 t          j        |           ||d<   d|d<   n3# t
          $ r&}t                              d||           Y d}~nd}~ww xY w|S )	z{Nuke the entire checkpoint base (store + legacy).  Irreversible.

    Returns ``{"bytes_freed": N, "deleted": bool}``.
    r   Fr  deletedr  Tr  z&Could not clear checkpoint base %s: %sN)r;   rh   r   r   r  rl   rj   rm   )r  r9   r   r  ro   s        r   	clear_allr  {  s    
 -oD
.
.C;;== 
4  DLd!MI L L L?sKKKKKKKKLJs   A 
BA??Bc                    | pt           }ddd}|                                s|S t          |                                          D ]}|                                r|j                            t                    s6	 t          |          }t          j
        |           |dxx         |z  cc<   |dxx         dz  cc<   {# t          $ r&}t                              d||           Y d}~d}~ww xY w|S )zhDelete all ``legacy-*`` archive directories.

    Returns ``{"bytes_freed": N, "deleted": count}``.
    r   r  r  r  r   z&Could not delete legacy archive %s: %sN)r;   rh   r   r   r   r   r   r   r   r   r  rl   rj   rm   )r  r9   r   r   r  ro   s         r   clear_legacyr    s    
 -oD
*
*C;;== 
dllnn%% 	Q 	Q||~~ 	UZ%:%:>%J%J 		Q"5))DM%   $&	NNNaNNNN 	Q 	Q 	QNNCUCPPPPPPPP	QJs   =AC
C1C,,C1rB   )NNT)r  TNr   N)r  r  TNr   )Or  r3   r   loggingr#   r5  r   r   r   pathlibr   hermes_constantsr   hermes_cli._subprocess_compatr   typingr   r   r   r	   r
   utilsr   	getLoggerr  rj   r;   r<   rH   rC   rK   r   r   r  minr   r  __annotations__r   compiler   r2   r   r,   r&   r8   r=   r?   rE   rI   rL   r   rb   rp   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r   r  r  r  r   r   objectr  r  r  r  r  r   r   <module>r$     s  0 0 0d    				 				             , , , , , , < < < < < < 3 3 3 3 3 3 3 3 3 3 3 3 3 3      		8	$	$ "/##m3   = = = @ CCCGG,G$L$LMMNNc N N N 
 "*344s x}     3 S Xc]    ,3 3 3 3 3 3
>s >s > > > >6 6htn 6 6 6 6 6
3 4    /t /s /t / / / /( ( ( ( ( (:d :c :d : : : : "&% %%% % 
	% % % %P$ 4    4  .2!%A# A#
s)A#A# A# 	A#
 "#c(+A# A# 4c>A# A# A# A#P- -$ - - - -`Bt B# B(3- B B B BJd t    BPT P P P P P P@Q$ QS QT Q Q Q Q:$ 4:    &#t #T
 # # # #L
# 
# 
 
 
 
$ 3    (4 c hsm    6v	& v	& v	& v	& v	& v	& v	& v	&rT
 s s    J # t # $     !% $$(	U UUU U "	U
 
U U U Up	$ 	4 	 	 	 	 &*&*R RRR d^R 	R
 smR 
#s(^R R R Rl  &*@ @@@ @ d^	@
 @ 
#v+@ @ @ @NQ Q(4. QD Q Q Q Qh x~ c3h    & (4. DcN      r   