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()