From 167a2d00edba22ecfbf24c4ab5a24f35a32c1da5 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Sat, 26 Sep 2026 23:09:50 +0530 Subject: [PATCH] fix(curator): purge ages archives by the newer of archived_at and dir mtime Preferring the usage record's archived_at outright let a stale archived_at beat a fresh dir mtime: set_state only rewrites archived_at on a state change, so a skill moved out of .archive by hand (record still "archived") and archived again keeps the old timestamp and is purged at once, even though archive_skill just stamped a fresh mtime. Take the newer of the two signals. Legacy archives (stale mtime, fresh archived_at) still survive, the stale-record case now survives too, and any disagreement errs toward keeping the archive, which is the no-data-loss direction for an irreversible purge. --- hermes_cli/curator.py | 7 ++++--- tests/hermes_cli/test_curator_archive_prune.py | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/hermes_cli/curator.py b/hermes_cli/curator.py index 18dbb63a89..1804944a02 100644 --- a/hermes_cli/curator.py +++ b/hermes_cli/curator.py @@ -440,11 +440,12 @@ def _cmd_purge(args) -> int: cutoff = time.time() - ttl_days * 86400 def _archived_ts(p: Path) -> float: - # Prefer the usage record's archived_at: archives made before archive_skill stamped the - # dir mtime still carry the skill's last-edit mtime and would be purged at once. + # The NEWER of the record's archived_at and the dir mtime: archives made before + # archive_skill stamped the mtime carry the skill's last-edit mtime, and a stale + # archived_at survives a manual un-archive + re-archive. Never purge before either says so. rec = skill_usage.get_record(p.name) at = skill_usage._parse_iso_timestamp(rec.get("archived_at")) if rec.get("state") == skill_usage.STATE_ARCHIVED else None - return at.timestamp() if at else p.stat().st_mtime + return max(at.timestamp(), p.stat().st_mtime) if at else p.stat().st_mtime candidates = sorted( p for p in archive_root.iterdir() if p.is_dir() and _archived_ts(p) < cutoff) diff --git a/tests/hermes_cli/test_curator_archive_prune.py b/tests/hermes_cli/test_curator_archive_prune.py index cd3501b341..5c3ff222d0 100644 --- a/tests/hermes_cli/test_curator_archive_prune.py +++ b/tests/hermes_cli/test_curator_archive_prune.py @@ -54,6 +54,10 @@ def test_purge_ages_an_archive_from_when_it_was_archived(tmp_path, monkeypatch): skill_usage.record_created("old-helper", agent_created=True) ok, msg = skill_usage.archive_skill("old-helper") assert ok, msg + # A stale archived_at (left by a manual un-archive + re-archive) must not beat the fresh mtime. + usage = skill_usage.load_usage() + usage["old-helper"]["archived_at"] = "2000-01-01T00:00:00+00:00" + assert skill_usage.save_usage(usage) assert curator_cli._cmd_purge(_ns(days=30, dry_run=False, yes=True)) == 0 assert (tmp_path / "skills" / ".archive" / "old-helper" / "SKILL.md").is_file()