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.
This commit is contained in:
kshitijk4poor
2026-09-26 23:09:50 +05:30
committed by kshitij
parent 59b77622a9
commit 167a2d00ed
2 changed files with 8 additions and 3 deletions

View File

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

View File

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