hermes - 💡(How to fix) Fix [Bug]: get_last_init_error() reads _last_init_error without the documented lock (hermes_state.py)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

hermes_state._set_last_init_error() protects the shared _last_init_error variable with _last_init_error_lock, and its docstring explicitly states: "Thread-safe via _last_init_error_lock."

However, get_last_init_error() reads the same variable without acquiring the lock:

# hermes_state.py:77
def _set_last_init_error(msg: Optional[str]) -> None:
    """...Thread-safe via _last_init_error_lock..."""
    global _last_init_error
    with _last_init_error_lock:   # ← lock acquired
        _last_init_error = msg

# hermes_state.py:94
def get_last_init_error() -> Optional[str]:
    """..."""
    return _last_init_error        # ← NO lock — races with the setter

Root Cause

The lock was added to the setter (presumably to protect against concurrent SessionDB() initialisations in the gateway), but the getter was not updated to match. The API contract ("thread-safe via lock") is only half-implemented.

Fix Action

Fix

Acquire _last_init_error_lock in the getter:

def get_last_init_error() -> Optional[str]:
    with _last_init_error_lock:
        return _last_init_error

One line change; the lock is already imported and initialised at module level.

Code Example

# hermes_state.py:77
def _set_last_init_error(msg: Optional[str]) -> None:
    """...Thread-safe via _last_init_error_lock..."""
    global _last_init_error
    with _last_init_error_lock:   # ← lock acquired
        _last_init_error = msg

# hermes_state.py:94
def get_last_init_error() -> Optional[str]:
    """..."""
    return _last_init_error        # ← NO lock — races with the setter

---

def get_last_init_error() -> Optional[str]:
    with _last_init_error_lock:
        return _last_init_error
RAW_BUFFERClick to expand / collapse

Description

hermes_state._set_last_init_error() protects the shared _last_init_error variable with _last_init_error_lock, and its docstring explicitly states: "Thread-safe via _last_init_error_lock."

However, get_last_init_error() reads the same variable without acquiring the lock:

# hermes_state.py:77
def _set_last_init_error(msg: Optional[str]) -> None:
    """...Thread-safe via _last_init_error_lock..."""
    global _last_init_error
    with _last_init_error_lock:   # ← lock acquired
        _last_init_error = msg

# hermes_state.py:94
def get_last_init_error() -> Optional[str]:
    """..."""
    return _last_init_error        # ← NO lock — races with the setter

Root cause

The lock was added to the setter (presumably to protect against concurrent SessionDB() initialisations in the gateway), but the getter was not updated to match. The API contract ("thread-safe via lock") is only half-implemented.

Impact

  • In CPython the assignment of a single object pointer is atomic (GIL), so this is unlikely to cause memory corruption in practice.
  • On non-CPython runtimes (PyPy, GraalPy) or if the GIL is removed (--disable-gil), this is a genuine data race.
  • The inconsistency misleads readers: the lock exists but isn't used symmetrically, making the locking intent unclear and fragile.
  • The gateway creates a new SessionDB() per request on multiple threads; any concurrent failure during start-up could race with a /resume handler calling get_last_init_error().

Fix

Acquire _last_init_error_lock in the getter:

def get_last_init_error() -> Optional[str]:
    with _last_init_error_lock:
        return _last_init_error

One line change; the lock is already imported and initialised at module level.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING