fix(browser_tool): resolve race in _get_command_timeout cache returning None (#14331)

# Conflicts:
#	tools/browser_tool.py
This commit is contained in:
Sanjay Santhanam 2026-06-29 02:10:00 -07:00 committed by Teknium
parent bf0d8fed8e
commit c79e6bceae
2 changed files with 131 additions and 5 deletions

View file

@ -244,10 +244,9 @@ def _get_command_timeout() -> int:
cached after the first call and cleared by ``cleanup_all_browsers()``.
"""
global _cached_command_timeout, _command_timeout_resolved
if _command_timeout_resolved:
return _cached_command_timeout # type: ignore[return-value]
if _command_timeout_resolved and _cached_command_timeout is not None:
return _cached_command_timeout
_command_timeout_resolved = True
result = DEFAULT_COMMAND_TIMEOUT
try:
from hermes_cli.config import read_raw_config
@ -257,10 +256,26 @@ def _get_command_timeout() -> int:
result = max(int(val), 5) # Floor at 5s to avoid instant kills
except Exception as e:
logger.debug("Could not read command_timeout from config: %s", e)
# Assign the cached value BEFORE flipping the resolved flag so a
# concurrent reader cannot observe ``resolved=True`` while the cache
# is still ``None`` (see issue #14331).
_cached_command_timeout = result
_command_timeout_resolved = True
return result
def _safe_command_timeout() -> int:
"""Like ``_get_command_timeout`` but guaranteed non-None.
Defense in depth against the race fixed in ``_get_command_timeout``:
if anything ever returns ``None`` (e.g. cache reset mid-flight), fall
back to ``DEFAULT_COMMAND_TIMEOUT``. Uses ``is not None`` rather than
``or`` so a legitimately configured ``0`` is preserved.
"""
val = _get_command_timeout()
return val if val is not None else DEFAULT_COMMAND_TIMEOUT
def _get_open_command_timeout(*, first_open: bool = False) -> int:
"""Timeout for agent-browser ``open`` (navigation / daemon cold start)."""
base = _get_command_timeout()
@ -2180,7 +2195,7 @@ def _run_browser_command(
Parsed JSON response from agent-browser
"""
if timeout is None:
timeout = _get_command_timeout()
timeout = _safe_command_timeout()
args = args or []
# Build the command
@ -4029,8 +4044,10 @@ def cleanup_all_browsers() -> None:
_cached_agent_browser = None
_agent_browser_resolved = False
_discover_homebrew_node_dirs.cache_clear()
_cached_command_timeout = None
# Flip the resolved flag BEFORE nulling the cache so a concurrent
# reader never sees ``resolved=True`` with ``cache=None`` (#14331).
_command_timeout_resolved = False
_cached_command_timeout = None
_cached_chromium_installed = None
global _chromium_autoinstall_attempted
_chromium_autoinstall_attempted = False