diff --git a/agent/skill_utils.py b/agent/skill_utils.py index a302c6981a4..c901ee30d9d 100644 --- a/agent/skill_utils.py +++ b/agent/skill_utils.py @@ -471,7 +471,7 @@ def _normalize_string_set(values) -> Set[str]: # which becomes the dominant cost of ``hermes`` startup when ~120 skills # each trigger a category lookup during banner construction (10+ seconds # of pure waste). -_EXTERNAL_DIRS_CACHE: Dict[Tuple[str, int], List[Path]] = {} +_EXTERNAL_DIRS_CACHE: Dict[Tuple[str, int, int], List[Path]] = {} def _external_dirs_cache_clear() -> None: @@ -496,11 +496,13 @@ def get_external_skills_dirs() -> List[Path]: if not config_path.exists(): return [] - # Cache key: (absolute path, mtime_ns). stat() is ~2us vs ~85ms for - # the full YAML parse, so the fast path is nearly free. + # Cache key: (absolute path, mtime_ns, size). stat() is ~2us vs ~85ms + # for the full YAML parse, so the fast path is nearly free. Size is in + # the key because overlayfs (CI runner pods) can coalesce rapid writes + # into a single mtime_ns tick. try: stat = config_path.stat() - cache_key: Tuple[str, int] = (str(config_path), stat.st_mtime_ns) + cache_key: Tuple[str, int, int] = (str(config_path), stat.st_mtime_ns, stat.st_size) except OSError: cache_key = None # type: ignore[assignment] diff --git a/gateway/run.py b/gateway/run.py index 911021a36ee..6bad574f3ef 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -21427,7 +21427,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew "honcho.runtime_peer_prefix", "honcho.user_peer_aliases", ) - _HONCHO_CACHE_BUSTING_MEMO: dict[tuple[str, int | None], dict[str, Any]] = {} + _HONCHO_CACHE_BUSTING_MEMO: dict[tuple[str, tuple[int, int] | None], dict[str, Any]] = {} @classmethod def _empty_honcho_cache_busting_config(cls) -> dict[str, Any]: @@ -21441,10 +21441,14 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew path = resolve_config_path() try: - mtime_ns = path.stat().st_mtime_ns + st = path.stat() + # mtime alone is not enough: overlayfs (CI runner pods) + # coalesces rapid writes into one mtime_ns tick. Size joins + # the key so a same-tick content change still re-parses. + stat_sig = (st.st_mtime_ns, st.st_size) except OSError: - mtime_ns = None - memo_key = (str(path), mtime_ns) + stat_sig = None + memo_key = (str(path), stat_sig) cached = cls._HONCHO_CACHE_BUSTING_MEMO.get(memo_key) if cached is not None: return dict(cached)