fix(skills): key the bundle cache on its directory, not mtime alone - #328
Merged
Conversation
`get_skill_bundles()` decided freshness by comparing the highest mtime across the bundles directory and its files against the cached snapshot's. The directory itself was not part of the key. `_bundles_dir()` resolves `<HERMES_HOME>/skill-bundles` at call time, so in a backend serving several profiles it points at a different home per request. mtime does not identify a profile: when two homes' newest bundle files happen to share an mtime, the freshness check saw no change and returned the snapshot built from the OTHER profile's directory. `/my-bundle` from one profile then resolved — or failed to resolve — against another profile's bundles. Equal mtimes are not exotic. Bundles are commonly seeded by the same install, copy, or checkout, which is exactly when several profiles' directories carry the same timestamp. Add the resolved directory to the key. A profile switch now always rescans, which is the correct trade: a rescan is a glob plus a few small YAML parses, and `get_skill_bundles()` already stat()s on every call. The ordinary single-home case still hits the cache — covered by a test that counts scans. `_bundles_cache` and `_bundles_cache_mtime` keep their names and shapes; six test files reset that pair between tests and are unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
get_skill_bundles()decided freshness by comparing the highest mtime across thebundles directory and its files against the cached snapshot's. The directory
itself was not part of the key.
_bundles_dir()resolves<HERMES_HOME>/skill-bundlesat call time, so in abackend serving several profiles it points at a different home per request. mtime
does not identify a profile: when two homes' newest bundle files happen to share an
mtime, the freshness check saw no change and returned the snapshot built from the
other profile's directory.
/my-bundlefrom one profile then resolved — orfailed to resolve — against another profile's bundles.
Equal mtimes are not an exotic condition. Bundles are commonly seeded by the same
install, copy, or checkout, which is precisely when several profiles' directories
carry the same timestamp.
The fix adds the resolved directory to the key. A profile switch now always
rescans. That is the correct trade: a rescan is a glob plus a few small YAML
parses, and
get_skill_bundles()alreadystat()s the directory and every bundlefile on each call, so the added cost on a switch is small and the cost on the
common path is zero.
This completes the
command.dispatchprofile-scoping audit alongside #323(
/undo), #324 (_finalize_session), and #326 (skill discovery).Related Issue
No filed issue — found while auditing
command.dispatchfor profile scoping.Type of Change
Changes Made
agent/skill_bundles.py— new module-level_bundles_cache_dir, set byscan_bundles()and compared inget_skill_bundles()alongside the existingmtime check. Comment records why mtime alone is not a sufficient key.
agent/skill_bundles.py—get_skill_bundles()docstring now states that adirectory change forces a rescan, so the behaviour is discoverable at the call
site rather than only in the diff.
tests/agent/test_skill_bundles.py— newTestCacheIsKeyedOnTheBundlesDir(3 tests).
Deliberately not reshaped:
_bundles_cacheand_bundles_cache_mtimekeeptheir existing names and types. Six test files across
tests/agent,tests/hermes_cli,tests/gateway,tests/cron, andtests/openviking_pluginreset that pair between tests; adding a third global leaves every one of those
reset idioms working. A per-directory cache dict would avoid the rescan-on-switch
entirely, but it would change that shared shape for a saving this call site does
not need.
How to Test
agent/skill_bundles.pyand runpytest tests/agent/test_skill_bundles.py -k TestCacheIsKeyedOnTheBundlesDir.Two of the three fail, showing one profile's bundles served to another:
..._same_dir_unchanged_still_hits_the_cache) passes either way bydesign — it guards against over-invalidating rather than reproducing the bug.
pytest tests/agent/test_skill_bundles.py -q→ 38 passed.reset the cache globals, plus skill-command discovery:
failed. The delta is exactly the three new tests; both runs are fully green,
so there is no pre-existing noise to account for here.
The test forces byte-identical mtimes across both homes with
os.utime— on dirsand files alike, since
_max_mtime()watches both. Without that the two homesdiffer by ordinary filesystem timestamps and the bug hides.
Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass — not claimed. The full suite is not green on this machine independent of this change. I ran the 7 bundle-consumer suites listed above, matched before and after: both fully green, 252 → 255.Documentation & Housekeeping
docs/, docstrings) — or N/A (get_skill_bundles()docstring updated; rationale comment added at the cache global)cli-config.yaml.exampleif I added/changed config keys — or N/A (N/A — no config keys)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A (N/A — internal cache key)str(Path), compared for equality only — no path semantics involved)Screenshots / Logs
Reproduction with two homes forced to identical mtimes — the process scans under
launch, then a request scoped toprofileasks again:With this change the second call returns
PROFILE.