Merge pull request #178 from nesquena/fix/streaming-env-lock-deadlock

fix: resolve _ENV_LOCK deadlock that blocks chat after first message
This commit is contained in:
Nathan Esquenazi
2026-04-08 07:26:53 -07:00
committed by GitHub
3 changed files with 26 additions and 12 deletions

View File

@@ -5,6 +5,17 @@
--- ---
## [v0.39.1] — 2026-04-08
### Bug Fixes
- **_ENV_LOCK deadlock resolved.** The environment variable lock was held for
the entire duration of agent execution (including all tool calls and streaming),
blocking all concurrent requests. Now the lock is acquired only for the brief
env variable read/write operations, released before the agent runs, and
re-acquired in the finally block for restoration.
---
## [v0.39.0] — 2026-04-08 ## [v0.39.0] — 2026-04-08
### Security (12 fixes — PR #171 by @betamod, reviewed by @nesquena-hermes) ### Security (12 fixes — PR #171 by @betamod, reviewed by @nesquena-hermes)

View File

@@ -107,6 +107,9 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
HERMES_HOME=_profile_home, HERMES_HOME=_profile_home,
) )
# Still set process-level env as fallback for tools that bypass thread-local # Still set process-level env as fallback for tools that bypass thread-local
# Acquire lock only for the env mutation, then release before the agent runs.
# The finally block re-acquires to restore — keeping critical sections short
# and preventing a deadlock where the restore would re-enter the same lock.
with _ENV_LOCK: with _ENV_LOCK:
old_cwd = os.environ.get('TERMINAL_CWD') old_cwd = os.environ.get('TERMINAL_CWD')
old_exec_ask = os.environ.get('HERMES_EXEC_ASK') old_exec_ask = os.environ.get('HERMES_EXEC_ASK')
@@ -117,8 +120,8 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
os.environ['HERMES_SESSION_KEY'] = session_id os.environ['HERMES_SESSION_KEY'] = session_id
if _profile_home: if _profile_home:
os.environ['HERMES_HOME'] = _profile_home os.environ['HERMES_HOME'] = _profile_home
# Lock released — agent runs without holding it
try: try:
def on_token(text): def on_token(text):
if text is None: if text is None:
return # end-of-stream sentinel return # end-of-stream sentinel
@@ -378,16 +381,16 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
usage['threshold_tokens'] = getattr(_cc, 'threshold_tokens', 0) or 0 usage['threshold_tokens'] = getattr(_cc, 'threshold_tokens', 0) or 0
usage['last_prompt_tokens'] = getattr(_cc, 'last_prompt_tokens', 0) or 0 usage['last_prompt_tokens'] = getattr(_cc, 'last_prompt_tokens', 0) or 0
put('done', {'session': s.compact() | {'messages': s.messages, 'tool_calls': tool_calls}, 'usage': usage}) put('done', {'session': s.compact() | {'messages': s.messages, 'tool_calls': tool_calls}, 'usage': usage})
finally: finally:
with _ENV_LOCK: with _ENV_LOCK:
if old_cwd is None: os.environ.pop('TERMINAL_CWD', None) if old_cwd is None: os.environ.pop('TERMINAL_CWD', None)
else: os.environ['TERMINAL_CWD'] = old_cwd else: os.environ['TERMINAL_CWD'] = old_cwd
if old_exec_ask is None: os.environ.pop('HERMES_EXEC_ASK', None) if old_exec_ask is None: os.environ.pop('HERMES_EXEC_ASK', None)
else: os.environ['HERMES_EXEC_ASK'] = old_exec_ask else: os.environ['HERMES_EXEC_ASK'] = old_exec_ask
if old_session_key is None: os.environ.pop('HERMES_SESSION_KEY', None) if old_session_key is None: os.environ.pop('HERMES_SESSION_KEY', None)
else: os.environ['HERMES_SESSION_KEY'] = old_session_key else: os.environ['HERMES_SESSION_KEY'] = old_session_key
if old_hermes_home is None: os.environ.pop('HERMES_HOME', None) if old_hermes_home is None: os.environ.pop('HERMES_HOME', None)
else: os.environ['HERMES_HOME'] = old_hermes_home else: os.environ['HERMES_HOME'] = old_hermes_home
except Exception as e: except Exception as e:
print('[webui] stream error:\n' + traceback.format_exc(), flush=True) print('[webui] stream error:\n' + traceback.format_exc(), flush=True)

View File

@@ -14,7 +14,7 @@
<body> <body>
<div class="layout"> <div class="layout">
<aside class="sidebar"> <aside class="sidebar">
<div class="sidebar-header"><div class="logo">H</div><div><h1 style="margin:0;font-size:15px;font-weight:700;letter-spacing:-.01em">Hermes</h1><div style="font-size:10px;color:var(--muted);opacity:.8;margin-top:1px">v0.39.0</div></div></div> <div class="sidebar-header"><div class="logo">H</div><div><h1 style="margin:0;font-size:15px;font-weight:700;letter-spacing:-.01em">Hermes</h1><div style="font-size:10px;color:var(--muted);opacity:.8;margin-top:1px">v0.39.1</div></div></div>
<div class="sidebar-nav"> <div class="sidebar-nav">
<button class="nav-tab active" data-panel="chat" data-label="Chat" onclick="switchPanel('chat')" title="Chat">&#128172;</button> <button class="nav-tab active" data-panel="chat" data-label="Chat" onclick="switchPanel('chat')" title="Chat">&#128172;</button>
<button class="nav-tab" data-panel="tasks" data-label="Tasks" onclick="switchPanel('tasks')" title="Tasks">&#128197;</button> <button class="nav-tab" data-panel="tasks" data-label="Tasks" onclick="switchPanel('tasks')" title="Tasks">&#128197;</button>