fix(skills): create reusing a dead skill's name gets archived by the curator (#65992) - #66288
fix(skills): create reusing a dead skill's name gets archived by the curator (#65992)#66288stantheman0128 wants to merge 3 commits into
Conversation
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Comment
Looks Good
- Fixes real bug: skill_manage(create) was incorrectly archiving newly created skills that reused the name of a previously removed skill
- Root cause well-explained: stale usage record left by removed skill had an expired inactivity clock, causing the automatic-transition pass to archive the new skill immediately
- Fix properly discards the stale usage record on successful create
- New test covers this exact scenario
Note
- 123 additions, 1 file changed — well-scoped fix
- No security concerns
Reviewed by Hermes Agent
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating the stale-record cause and covering the ordinary sequential create → transition path.
Problems
tools/skill_manager_tool.py:846removes the stale record, but an already-running pass can have obtained its stale row atagent/curator.py:328. That pass later callsarchive_skill(name)atagent/curator.py:371-374;archive_skill()resolves and renames the current directory without checking the usage record again (tools/skill_usage.py:704-740). The fresh skill can therefore still be moved to.archivein the interleaving described in the PR body.
Suggested changes
- Revalidate the live usage record under coordinated lifecycle locking immediately before archival, and add a barrier-based regression for the snapshot/create/archive ordering.
This is an automated hermes-sweeper review.
| # Best-effort: telemetry failures never break the tool. | ||
| try: | ||
| from tools.skill_usage import forget | ||
| forget(name) |
There was a problem hiding this comment.
forget() fixes the sequential path, but it cannot invalidate a stale row already returned by agent_created_report(). That pass can still reach archive_skill(name), which resolves and renames the newly created directory without checking the record again. Please coordinate the create reset with archive-time revalidation/locking and cover that interleaving.
|
Thanks for catching the in-flight race. Tightened that path in f2f76a328. What changed:
Evidence (Windows 11, Python 3.13 venv): The one failure is pre-existing:
|
…ll's name skill_manage(action="create") reported success with the requested skills/<category>/<name>/SKILL.md path, but the file could end up under skills/.archive/<name>/ moments later (NousResearch#65992). Usage records in .usage.json are keyed by skill name and survive any removal that did not go through skill_manage(delete) (manual rm, crashed delete), so a new skill reusing the name inherited the previous life's expired inactivity clock. The curator's next automatic-transition pass read that anchor, decided the seconds-old skill was long inactive, and archive_skill() relocated it, flattening the category exactly as reported. A successful create now starts a new life: any leftover record for the name is forgotten (the collision check guarantees no live skill owns it), matching what a hard delete already does. Covers both callers, skill_manage and the dashboard's create endpoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ousResearch#65992) Reproduces the reported sequence against a temp HERMES_HOME with real imports: seed a stale agent-created usage record, create a fresh skill under a category via skill_manage, run apply_automatic_transitions, and assert the skill stays at its reported path instead of being relocated to skills/.archive/<name>/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…65992) Close the in-flight curator race where a stale snapshot can still archive a skill that create just reset. Co-authored-by: Cursor <cursoragent@cursor.com>
f2f76a3 to
10f3f9c
Compare
|
Closing as author to bring our open PRs on hermes-agent back within a healthy throttle (we had 10 open with only sweeper keep_open and no concrete maintainer change requests for days). Keeping three Windows-focused PRs open for now:
Happy to reopen this one if a maintainer wants it prioritized. Thanks for the patience. |
What does this PR do?
Reproduces and fixes #65992:
skill_manage(action="create", name="test-skill", category="devops")returns success withskill_mdpointing atskills/devops/test-skill/SKILL.md, but the file ends up atskills/.archive/test-skill/SKILL.md, and subsequentskill_view/patchcalls fail with "not found".Root cause:
.usage.jsonrecords are keyed by skill name and survive any removal that did not go throughskill_manage(delete)(a manualrm -rf, a crashed delete). When a create reuses such a name, the new skill inherits the dead record's inactivity clock. The curator's nextapply_automatic_transitions()pass (kicked off on CLI startup in a daemon thread, and from gateway housekeeping) reads the expired anchor, decides the seconds-old skill is long inactive, andarchive_skill()relocates the directory toskills/.archive/<name>/, flattening the category. That is exactly the reported filesystem shape. It also explains why checkingarchive_after_days=90against the skill's age did not implicate the curator: the clock that matters is the record's, not the directory's. The reporter's code analysis was correct that the create path never references.archive; the relocation happens in the pass that follows.Fix: a successful create now starts a new life for the name by discarding any leftover usage record (
skill_usage.forget), the same reset a hard delete already performs. The collision check in_create_skillguarantees no live skill owns the record at that point. Placing the reset inside_create_skillcovers both callers, theskill_managetool and the dashboard's create endpoint.One limitation, stated for review: if a curator pass is already mid-walk when the create lands, the pass still holds a snapshot row with the stale clock, so an archive in that narrow window remains possible. Closing it would require the prune to revalidate the live record under the usage lock right before the destructive move. I kept this PR to the deterministic root cause and can follow up if that hardening is wanted.
Related: #24068 adds a manual
hermes curator repair-usagecommand that reconciles orphan records. Complementary, but it does not prevent this bug, since nothing runs it automatically between the create and the curator pass.Related Issue
Fixes #65992
Type of Change
Changes Made
tools/skill_manager_tool.py:_create_skill()forgets any leftover usage record after a successful write and security scan (best-effort, same error posture as the existing telemetry block).tools/skill_usage.py:forget()docstring now documents the second caller.tests/tools/test_skill_manager_tool.py: unit tests. A create discards a stale record from a previous life; a failed duplicate create leaves the live skill's record untouched.tests/agent/test_curator_activity.py: end-to-end regression against a tempHERMES_HOMEwith real imports. Seeds a stale agent-created record, creates a fresh skill under a category viaskill_manage, runsapply_automatic_transitions, and asserts the skill stays at its reported path instead of moving toskills/.archive/<name>/.How to Test
scripts/run_tests.sh tests/agent/test_curator_activity.py tests/tools/test_skill_manager_tool.pytests/agent/test_curator_activity.py::test_fresh_create_reusing_dead_skill_name_is_not_archivedon a checkout without the fix: the pass archives the seconds-old skill (counts["archived"] == 1) and the file moves toskills/.archive/fresh-skill/while the create response still points atskills/devops/fresh-skill/SKILL.md.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass (I ran the four affected suites per file instead: test_skill_manager_tool.py, test_curator_activity.py, test_skill_usage.py, test_curator.py; 224 passed. One pre-existing failure on my machine,test_symlinked_skill_dir_refused, fails identically on main because unelevated Windows cannot create symlinks, WinError 1314.)Documentation & Housekeeping
docs/, docstrings) — updated theforget()docstringcli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/Ascripts/check-windows-footguns.pyis clean on all touched filesScreenshots / Logs
Standalone repro on current main (temp
HERMES_HOME, stale agent-created record fortest-skillwithlast_used_at200 days old, then create + one automatic-transition pass):Same script with this fix applied:
New regression test, red before the fix, green after:
Transparency: I diagnosed and developed this fix with an AI coding assistant (Claude) under my direction and reviewed the diagnosis, diff, and test evidence myself.