fix(plugins): preserve cron registry from disk-cleanup auto-categorisation#32177
fix(plugins): preserve cron registry from disk-cleanup auto-categorisation#32177briandevans wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Updates disk-cleanup auto-tracking so cron scheduler control-plane files aren’t mistakenly classified as deletable “cron-output”, preventing accidental deletion of the job registry.
Changes:
- Narrow
guess_category()’scron/cronjobsclassification to onlycron/output/**. - Add regression tests ensuring cron registries/locks are never categorized as
cron-output. - Update the existing cron test to target a realistic
cron/output/<job>/...subtree.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| tests/plugins/test_disk_cleanup_plugin.py | Adds/adjusts tests for cron output vs durable cron registry files. |
| plugins/disk-cleanup/disk_cleanup.py | Restricts cron categorization to cron/output/** to avoid deleting scheduler state. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| p = cron_dir / "job_output.md" | ||
| output_dir = _isolate_env / "cron" / "output" / "myjob" | ||
| output_dir.mkdir(parents=True) | ||
| p = output_dir / "run.md" | ||
| p.write_text("x") | ||
| assert dg.guess_category(p) == "cron-output" | ||
|
|
| def test_cronjobs_registry_not_classified(self, _isolate_env): | ||
| # ``cronjobs`` is the legacy alias for the same top-level dir; | ||
| # the same registry-preservation invariant must hold. | ||
| dg = _load_lib() | ||
| cron_dir = _isolate_env / "cronjobs" | ||
| cron_dir.mkdir() | ||
| p = cron_dir / "jobs.json" | ||
| p.write_text("[]") | ||
| assert dg.guess_category(p) is None |
| def test_cron_registry_not_classified(self, _isolate_env): | ||
| # Regression for #32164: the live scheduler registry must never | ||
| # be auto-tracked as ``cron-output`` or quick cleanup will delete | ||
| # it and Hermes will then read missing-registry as zero jobs. | ||
| dg = _load_lib() | ||
| cron_dir = _isolate_env / "cron" | ||
| cron_dir.mkdir() | ||
| p = cron_dir / "jobs.json" | ||
| p.write_text("[]") | ||
| assert dg.guess_category(p) is None | ||
|
|
||
| def test_cron_tick_lock_not_classified(self, _isolate_env): | ||
| # ``cron/.tick.lock`` is also durable control-plane state; same | ||
| # invariant as ``jobs.json``. | ||
| dg = _load_lib() | ||
| cron_dir = _isolate_env / "cron" | ||
| cron_dir.mkdir() | ||
| p = cron_dir / ".tick.lock" | ||
| p.write_text("") | ||
| assert dg.guess_category(p) is None | ||
|
|
||
| def test_cronjobs_registry_not_classified(self, _isolate_env): | ||
| # ``cronjobs`` is the legacy alias for the same top-level dir; | ||
| # the same registry-preservation invariant must hold. | ||
| dg = _load_lib() | ||
| cron_dir = _isolate_env / "cronjobs" | ||
| cron_dir.mkdir() | ||
| p = cron_dir / "jobs.json" | ||
| p.write_text("[]") | ||
| assert dg.guess_category(p) is None | ||
|
|
…ation `guess_category()` classified every top-level path under `HERMES_HOME/cron/` (and the legacy `cronjobs/` alias) as `cron-output`, which makes the file eligible for unconditional deletion after 14 days in `quick()`. That is correct for run artifacts under `cron/output/<job>/` but it also catches durable scheduler state at the top of the dir: - `HERMES_HOME/cron/jobs.json` — the scheduler's job registry - `HERMES_HOME/cron/.tick.lock` — the scheduler's tick mutex When `disk-cleanup`'s `post_tool_call` hook auto-tracks one of those paths (any tool that touches it through the safe-path filter qualifies) the next quick cleanup pass deletes the registry; Hermes then loads the missing file as an empty job list and the user's entire cron schedule silently disappears. Restrict `cron-output` classification to paths under `cron/output/**` (and `cronjobs/output/**`); top-level cron control files now correctly fall through to `None` and are never auto-tracked. Regression tests cover the three known durable paths plus the legacy `cronjobs/` alias. The existing happy-path test is reshaped around the new `cron/output/<job>/` layout to keep coverage on the disposable side. Fixes NousResearch#32164.
bd1d47f to
4207fe1
Compare
|
Closing — the cron-registry preservation this PR adds is already on |
What does this PR do?
plugins/disk-cleanup'sguess_category()classified every top-level path underHERMES_HOME/cron/(and the legacycronjobs/alias) ascron-output. That is correct for the per-job run artifacts undercron/output/<job>/, but it also catches durable scheduler state —cron/jobs.json(the live job registry) andcron/.tick.lock(the tick mutex). Once thepost_tool_callhook auto-tracksjobs.jsonundercron-output, the nextquick()pass deletes the registry; Hermes then interprets the missing file as an empty schedule and the user's entire cron list silently disappears.This PR narrows the
cron-outputbranch to paths insidecron/output/**(andcronjobs/output/**for the legacy alias); top-level control-plane files now fall through toNoneand are never auto-tracked. The fix is the one written into the issue body verbatim, with regression tests for the three durable paths the issue calls out.Related Issue
Fixes #32164
Type of Change
Changes Made
plugins/disk-cleanup/disk_cleanup.py—guess_category()now requiresrel.parts[1] == \"output\"before returningcron-output; top-level cron files fall through toNone.tests/plugins/test_disk_cleanup_plugin.py— reshapetest_cron_subtree_categorisedaround the newcron/output/<job>/run.mdlayout; addtest_cron_registry_not_classified,test_cron_tick_lock_not_classified, andtest_cronjobs_registry_not_classifiedas regression guards.How to Test
main: with the patch reverted,guess_category(Path('~/.hermes/cron/jobs.json'))returns'cron-output', which means a singlepost_tool_callauto-track + quick cleanup pass (14 days later) deletes the registry.guess_category(Path('~/.hermes/cron/jobs.json'))returnsNone;guess_category(Path('~/.hermes/cron/output/myjob/run.md'))still returns'cron-output'.uv run --with pytest --with pytest-xdist --with pytest-asyncio python3 -m pytest tests/plugins/test_disk_cleanup_plugin.py -v→ 41 passed locally.Regression-guard pass: stashed
plugins/disk-cleanup/disk_cleanup.pyand re-ran the three new tests on the unpatched code — all three failed with'cron-output' is None, confirming they exercise the bug.Checklist
Code
fix(scope):,feat(scope):, etc.)Documentation & Housekeeping
docs/, docstrings) — or N/A (inline comment explains the carve-out; existing module docstring is still accurate)cli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/APath.partsso the same logic applies on WindowsScreenshots / Logs