mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
fix(profile): propagate profile context across thread/executor boundaries
A bare threading.Thread / ThreadPoolExecutor worker starts with an empty contextvars.Context, so the context-local profile override (_HERMES_HOME_OVERRIDE) does not cross the spawn boundary. In single-process multi-profile runtimes (desktop tui_gateway) the worker then resolves get_hermes_home() to the launch/default profile, leaking one profile's reads/writes into another. The fix primitive (tools.thread_context. propagate_context_to_thread, which copies the parent context) already exists; the leaking spawns simply did not use it. - model_tools.py _run_async: wrap the worker-thread loop runner. This is the generic sync->async bridge for every async tool, so wrapping it here fixes the leak for all async tools at once (verified: an async tool reading get_hermes_home() under an override now resolves the active profile). - run_agent.py bg-review thread: wrap so MEMORY.md / skill review writes land in the spawning turn's profile (#54937 path). - tools/async_delegation.py: wrap both single + batch executor.submit calls so detached children resolve the dispatching profile's paths. Scope: the vision CPU executor is intentionally left unwrapped — it runs pure in-memory encode/resize and never resolves profile-scoped paths.
This commit is contained in:
parent
10e60060d9
commit
09af0a8c1d
3 changed files with 30 additions and 4 deletions
|
|
@ -144,7 +144,15 @@ def _run_async(coro):
|
|||
worker_loop.close()
|
||||
|
||||
pool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
||||
future = pool.submit(_run_in_worker)
|
||||
# Propagate the parent thread's ContextVars (notably the
|
||||
# _HERMES_HOME_OVERRIDE profile scope) and approval/sudo callbacks into
|
||||
# the worker thread. Without this, any async tool that resolves
|
||||
# get_hermes_home() inside its coroutine falls back to the launch/default
|
||||
# profile in single-process multi-profile runtimes (desktop tui_gateway),
|
||||
# leaking one profile's reads/writes into another.
|
||||
from tools.thread_context import propagate_context_to_thread
|
||||
|
||||
future = pool.submit(propagate_context_to_thread(_run_in_worker))
|
||||
try:
|
||||
return future.result(timeout=300)
|
||||
except concurrent.futures.TimeoutError:
|
||||
|
|
|
|||
11
run_agent.py
11
run_agent.py
|
|
@ -1525,13 +1525,22 @@ class AIAgent:
|
|||
keep working.
|
||||
"""
|
||||
from agent.background_review import spawn_background_review_thread
|
||||
from tools.thread_context import propagate_context_to_thread
|
||||
target, _prompt = spawn_background_review_thread(
|
||||
self,
|
||||
messages_snapshot,
|
||||
review_memory=review_memory,
|
||||
review_skills=review_skills,
|
||||
)
|
||||
t = threading.Thread(target=target, daemon=True, name="bg-review")
|
||||
# Propagate the spawning turn's ContextVars (notably the
|
||||
# _HERMES_HOME_OVERRIDE profile scope) into the review thread. A bare
|
||||
# threading.Thread starts with an empty context, so the review would
|
||||
# resolve get_hermes_home() to the launch/default profile and write
|
||||
# MEMORY.md / skill review into the wrong profile in single-process
|
||||
# multi-profile runtimes (desktop tui_gateway) — see #54937.
|
||||
t = threading.Thread(
|
||||
target=propagate_context_to_thread(target), daemon=True, name="bg-review"
|
||||
)
|
||||
t.start()
|
||||
|
||||
def _build_memory_write_metadata(
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ from concurrent.futures import ThreadPoolExecutor
|
|||
from concurrent.futures.thread import _worker
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from tools.thread_context import propagate_context_to_thread
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -247,7 +249,11 @@ def dispatch_async_delegation(
|
|||
_finalize(delegation_id, result, status)
|
||||
|
||||
try:
|
||||
executor.submit(_worker)
|
||||
# Capture the dispatching turn's ContextVars (notably the
|
||||
# _HERMES_HOME_OVERRIDE profile scope) so the detached child resolves
|
||||
# get_hermes_home() under the right profile in single-process
|
||||
# multi-profile runtimes (desktop tui_gateway).
|
||||
executor.submit(propagate_context_to_thread(_worker))
|
||||
except Exception as exc: # pragma: no cover — pool submit failure is rare
|
||||
with _records_lock:
|
||||
_records.pop(delegation_id, None)
|
||||
|
|
@ -432,7 +438,10 @@ def dispatch_async_delegation_batch(
|
|||
_finalize_batch(delegation_id, combined, status)
|
||||
|
||||
try:
|
||||
executor.submit(_worker)
|
||||
# Capture the dispatching turn's ContextVars (notably the
|
||||
# _HERMES_HOME_OVERRIDE profile scope) so the detached batch children
|
||||
# resolve get_hermes_home() under the right profile.
|
||||
executor.submit(propagate_context_to_thread(_worker))
|
||||
except Exception as exc: # pragma: no cover
|
||||
with _records_lock:
|
||||
_records.pop(delegation_id, None)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue