fix(plugins): isolate ownership by profile

This commit is contained in:
doncazper
2026-08-01 20:03:12 -07:00
committed by Teknium
parent 4e1b2e436c
commit 85020f2238
29 changed files with 3101 additions and 479 deletions
+17 -7
View File
@@ -274,7 +274,7 @@ _LEGACY_TOOLSET_MAP = {
# =============================================================================
# Module-level memoization for get_tool_definitions(). Keyed on
# (frozenset(enabled_toolsets), frozenset(disabled_toolsets), registry._generation).
# (profile scope, enabled/disabled toolsets, registry generation).
# Hot callers (gateway runner, AIAgent.__init__) invoke this on every turn
# with quiet_mode=True; caching avoids ~7 ms of registry walking + schema
# filtering + check_fn probing per call. Only active when quiet_mode=True
@@ -285,6 +285,7 @@ _LEGACY_TOOLSET_MAP = {
# inner check_fn TTL cache in registry.py handles environment drift (Docker
# daemon start/stop, env var changes, etc.) on a 30 s horizon.
_tool_defs_cache: Dict[tuple, List[Dict[str, Any]]] = {}
_tool_defs_cache_lock = threading.Lock()
# Hard cap on memoized get_tool_definitions() results. A long-lived Gateway
# process sees many distinct toolset/config fingerprints over its lifetime
@@ -299,7 +300,8 @@ def _clear_tool_defs_cache() -> None:
"""Drop memoized get_tool_definitions() results. Called when dynamic
schema dependencies change (e.g. discord capability cache reset,
execute_code sandbox reconfigured)."""
_tool_defs_cache.clear()
with _tool_defs_cache_lock:
_tool_defs_cache.clear()
def get_tool_definitions(
@@ -346,6 +348,7 @@ def get_tool_definitions(
profile_scope = check_fn_cache_scope()
if profile_scope != CHECK_FN_CACHE_BYPASS:
cache_key = (
registry.current_scope_key(),
frozenset(enabled_toolsets) if enabled_toolsets is not None else None,
frozenset(disabled_toolsets) if disabled_toolsets else None,
registry._generation,
@@ -356,7 +359,8 @@ def get_tool_definitions(
_is_dispatcher_owned_worker(),
profile_scope,
)
cached = _tool_defs_cache.get(cache_key) if cache_key is not None else None
with _tool_defs_cache_lock:
cached = _tool_defs_cache.get(cache_key) if cache_key is not None else None
if cached is not None:
# Update _last_resolved_tool_names so downstream callers see
# consistent state even on a cache hit.
@@ -379,10 +383,16 @@ def get_tool_definitions(
# Bound the cache with LRU eviction so a long-lived Gateway process
# doesn't accumulate entries unboundedly across the many distinct
# toolset/config fingerprints it sees over its lifetime (#19251).
if len(_tool_defs_cache) >= _TOOL_DEFS_CACHE_MAX:
_tool_defs_cache.pop(next(iter(_tool_defs_cache))) # evict oldest
_tool_defs_cache[cache_key] = result
return list(result)
with _tool_defs_cache_lock:
# Another thread may have populated this exact key while this
# thread computed. Reuse it and serialize capacity eviction.
cached = _tool_defs_cache.get(cache_key)
if cached is None:
if len(_tool_defs_cache) >= _TOOL_DEFS_CACHE_MAX:
_tool_defs_cache.pop(next(iter(_tool_defs_cache)))
_tool_defs_cache[cache_key] = result
cached = result
return list(cached)
if quiet_mode:
return list(result)
return result