mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
perf(tools): harden manifest cache — atomic writes + per-checkout scoping
Follow-ups on top of the salvaged manifest cache: - atomic os.replace() write so concurrent cold-starting processes (gateway + CLI + cron) never read a torn JSON manifest - validate the stored tools_dir so a shared HERMES_HOME serving several checkouts (main clone + worktrees) never serves a stale module list built from a different tree, plus a regression test for it E2E (isolated HERMES_HOME, median of 6): import model_tools 375 ms (scan) -> 235 ms (manifest hit), -140 ms per cold process. Hit path returns the identical 34-module list as a full scan; touch- invalidation and corrupted-manifest fallback both verified cross-process.
This commit is contained in:
parent
7251e71ff2
commit
c26398ccf1
2 changed files with 24 additions and 1 deletions
|
|
@ -64,6 +64,19 @@ class TestManifestCache:
|
|||
assert loaded is not None
|
||||
assert loaded == ["tools.alpha", "tools.beta"]
|
||||
|
||||
def test_cache_miss_when_tools_dir_differs(self, tool_dir, cache_path, tmp_path):
|
||||
"""A manifest built for one checkout must not serve another checkout."""
|
||||
module = _load_registry_module()
|
||||
module._save_manifest_cache(cache_path, tools_dir=tool_dir, module_names=["tools.alpha", "tools.beta"])
|
||||
other = tmp_path / "other" / "tools"
|
||||
other.mkdir(parents=True)
|
||||
# Mirror the exact files/mtimes so only tools_dir differs.
|
||||
import shutil
|
||||
for p in tool_dir.glob("*.py"):
|
||||
dest = other / p.name
|
||||
shutil.copy2(p, dest)
|
||||
assert module._load_manifest_cache(cache_path, tools_dir=other) is None
|
||||
|
||||
def test_cache_miss_when_file_missing(self, tool_dir, cache_path):
|
||||
module = _load_registry_module()
|
||||
assert module._load_manifest_cache(cache_path, tools_dir=tool_dir) is None
|
||||
|
|
|
|||
|
|
@ -98,7 +98,11 @@ def _save_manifest_cache(cache_path: Path, tools_dir: Path, module_names: List[s
|
|||
"module_names": module_names,
|
||||
"mtimes": _collect_tool_file_mtimes(tools_dir),
|
||||
}
|
||||
cache_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
# Atomic replace so a concurrent reader (gateway + CLI + cron can all
|
||||
# cold-start at once) never sees a torn/partial JSON file.
|
||||
tmp_path = cache_path.with_name(cache_path.name + f".tmp{os.getpid()}")
|
||||
tmp_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
os.replace(tmp_path, cache_path)
|
||||
except Exception:
|
||||
logger.debug("Failed to save tool manifest cache", exc_info=True)
|
||||
|
||||
|
|
@ -113,6 +117,12 @@ def _load_manifest_cache(cache_path: Path, tools_dir: Path) -> Optional[List[str
|
|||
return None
|
||||
if not isinstance(data, dict) or data.get("version") != 1:
|
||||
return None
|
||||
# A shared HERMES_HOME can be used by several checkouts (main clone +
|
||||
# worktrees). The manifest is only valid for the tools dir it was built
|
||||
# from — otherwise identical mtimes across copies could serve a stale
|
||||
# module list from a different tree.
|
||||
if data.get("tools_dir") != str(tools_dir):
|
||||
return None
|
||||
cached_mtimes = data.get("mtimes", {})
|
||||
current_mtimes = _collect_tool_file_mtimes(tools_dir)
|
||||
if cached_mtimes != current_mtimes:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue