Skip to content

Hermes plugin: stable project scoping + close sessions on process exit#891

Open
zuevrs wants to merge 2 commits into
rohitg00:mainfrom
zuevrs:fix/hermes-plugin-project-scope-and-session-close
Open

Hermes plugin: stable project scoping + close sessions on process exit#891
zuevrs wants to merge 2 commits into
rohitg00:mainfrom
zuevrs:fix/hermes-plugin-project-scope-and-session-close

Conversation

@zuevrs

@zuevrs zuevrs commented Jun 10, 2026

Copy link
Copy Markdown

What

Two independent fixes to the Hermes integration (integrations/hermes/__init__.py), found by running the plugin in production under a headless ACP-driven Hermes deployment that checks every task out into an ephemeral git worktree. Both are generic - any worktree/CI/orchestrator layout hits them.

1. Scope memory by git remote name, not the checkout path (commit 1)

Problem: initialize() used the session cwd as the agentmemory project id. A checkout path is not a stable repo identity: ephemeral checkouts (git worktrees, CI runners, agent-orchestrator workdirs) place the same repo under throwaway paths, and different repos often share one checkout basename (workdir, workspace, ...). In our deployment every task ran in .../<task-hash>/workdir - all projects collapsed into one shared workdir memory bucket, and recall started returning Go lessons inside TypeScript tasks.

Fix: resolve the project id as

  1. AGENTMEMORY_PROJECT_NAME env var (explicit override),
  2. git origin remote repo name - of the cwd or of a direct child checkout (covers sessions started one level above the repo),
  3. git toplevel basename,
  4. cwd basename (the previous behavior, as last resort).

The real cwd is still sent as cwd, and the resolved project is now passed explicitly on the search / remember / smart-search calls so recall and save stay inside the project bucket.

2. End the session on interpreter exit (commit 2)

Problem: Hermes' cli and gateway paths call on_session_end() / shutdown(), but the ACP adapter path (headless editors / daemons) tears the process down without either - every such run leaked its agentmemory session as permanently "active" on the server.

Fix: register an atexit hook at initialize() and route all teardown paths (on_session_end, shutdown, atexit) through an idempotent _close_session(), so session/end is POSTed exactly once on any graceful exit regardless of which host path ran. Hard kills (SIGKILL) can't be caught - that remains the server-side idle sweep's job.

How to verify

  1. Project scoping: git worktree add /tmp/wt-a <repo-a> and start a Hermes session there, repeat with a second repo in /tmp/wt-b; with this patch session/start carries project: <repo-a-name> / project: <repo-b-name> instead of wt-a / wt-b (or one shared basename). A dir without any git remote falls back to its basename exactly as before.
  2. Session close: run a Hermes session through the ACP adapter (e.g. Zed), exit the editor, and check the server's active sessions - the session is now ended instead of dangling.
  3. Python-only change; npm run build / npm test unaffected. python3 -m py_compile integrations/hermes/__init__.py passes; _resolve_project() exercised against the four layouts above (worktree-with-remote, parent-of-checkout, plain dir, env override).

Both commits are DCO signed-off. No issue filed for these - happy to split this into two issues/PRs if you prefer one logical change per PR taken strictly.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved session lifecycle management to ensure graceful shutdowns are handled reliably with proper cleanup on interpreter exit
    • Enhanced project identification logic using git remote metadata with intelligent fallback mechanisms
  • New Features

    • Automatic preloading of environment configuration files at startup for consistent setup across sessions
    • Better scoping of tool requests to include proper project and working directory context

Roman Zuev added 2 commits June 10, 2026 22:49
The Hermes plugin used the session cwd as the agentmemory project id.
A cwd path is not a stable identity for a repo: ephemeral checkouts
(git worktrees, CI runners, agent-orchestrator workdirs) put the same
repo under throwaway paths, and unrelated repos often share a checkout
basename like 'workdir' - so one repo's memory fragments across paths
while different repos cross-bleed into one shared bucket.

Resolve the project id as: AGENTMEMORY_PROJECT_NAME env override ->
git origin remote repo name (of cwd or a direct child checkout) ->
git toplevel basename -> cwd basename (previous behavior). Send the
real cwd separately, and pass the resolved project explicitly on the
search / remember / smart-search tool calls so recall and save stay
inside the project bucket.

Signed-off-by: Roman Zuev <[email protected]>
Hermes' cli and gateway paths call on_session_end()/shutdown(), but the
ACP adapter path (headless editors and daemons) tears the process down
without either - every such run left its agentmemory session dangling
as 'active' on the server. Register an atexit hook at initialize() and
route all teardown paths through an idempotent _close_session(), so the
session is ended exactly once on any graceful exit regardless of which
host path ran. Hard kills remain the server-side idle sweep's job.

Signed-off-by: Roman Zuev <[email protected]>
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The Hermes plugin now derives stable project identity from git remotes and working directory, captures cwd alongside project during session initialization, registers graceful atexit shutdown, includes project in memory tool requests, and centralizes session teardown through an idempotent _close_session() method.

Changes

Hermes session lifecycle and project scoping

Layer / File(s) Summary
Project resolution infrastructure
integrations/hermes/__init__.py
subprocess import added; new internal helpers _project_from_remote() and _resolve_project() determine stable project identity via environment override, git origin parsing, git toplevel basename, and cwd basename fallback.
Session initialization with cwd and project capture
integrations/hermes/__init__.py
initialize() now captures session cwd, resolves _project using the new helpers, passes both to session/start, enforces plaintext-bearer guard for HTTP configs, and registers atexit handler to ensure graceful session closure on interpreter exit.
Memory tool request scoping with project
integrations/hermes/__init__.py
memory_recall, memory_save, and memory_search tool handlers now include the resolved project field in request payloads for consistent session-scoped queries and mutations.
Centralized session teardown and sync_turn observation
integrations/hermes/__init__.py
New idempotent _close_session() method guards against double-ending, unregisters from atexit, and issues single /session/end call; on_session_end() and shutdown() invoke it. sync_turn()'s observe payload now sends stored session cwd instead of project.

Sequence Diagram(s)

sequenceDiagram
  participant Client as initialize()
  participant Resolver as _resolve_project()
  participant Session as Remote Session
  participant MemoryTools as Memory Tools
  participant Atexit as atexit Handler
  
  Client->>Resolver: resolve_project(cwd)
  Resolver-->>Client: project_id
  Client->>Session: POST /session/start(sessionId, project, cwd)
  Session-->>Client: session created
  Client->>Atexit: register _close_session()
  
  MemoryTools->>Session: POST /memory/recall(project, query, limit)
  Session-->>MemoryTools: results
  
  Note over Atexit,Session: On interpreter exit or explicit shutdown:
  Atexit->>Atexit: _close_session() idempotent guard
  Atexit->>Session: POST /session/end once
  Session-->>Atexit: session ended
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • rohitg00/agentmemory#643: Expands Hermes import-time .env preloading and guarantees AGENTMEMORY_URL is set, overlapping with this PR's .env preload logic.
  • rohitg00/agentmemory#579: Both PRs refactor session-lifecycle handling to ensure explicit /session/end POST is made during shutdown via centralized teardown.

Poem

🐰 Sessions now have stable homes,
Git roots guide each roaming tome,
With cwd and project bound,
Clean shutdown without a sound!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and concisely captures the two main changes: establishing stable project scoping via git remotes and ensuring sessions close on process exit, which are the core objectives of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
integrations/hermes/__init__.py (3)

26-45: ⚡ Quick win

Consider logging git command failures for troubleshooting.

The function silently returns "" on any git failure, which provides graceful degradation but makes it harder to diagnose issues when git is misconfigured or unavailable. Consider adding optional debug logging for the exception case to help users troubleshoot setup problems.

💡 Example: Add debug logging
+import logging
+
+_logger = logging.getLogger(__name__)
+
 def _project_from_remote(path: str) -> str:
     """Repo name from ``git remote get-url origin``, or "" if unavailable.
 
     Parses both remote forms: ``git@host:owner/repo(.git)`` and
     ``https://host/owner/repo(.git)`` -> ``repo``.
     """
     try:
         url = subprocess.run(
             ["git", "-C", path, "remote", "get-url", "origin"],
             capture_output=True, text=True, timeout=2,
         ).stdout.strip()
-    except Exception:
+    except Exception as e:
+        _logger.debug("Failed to read git remote from %s: %s", path, e)
         return ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/hermes/__init__.py` around lines 26 - 45, The
_project_from_remote function swallows all exceptions and returns an empty
string, making failures hard to diagnose; update the except block in
_project_from_remote to optionally log the caught exception (e.g., using the
module logger via logging.getLogger(__name__)) at debug or warning level with
context like "git remote get-url origin failed for path=%s" before returning "",
so callers still get graceful degradation but developers can enable logs to
troubleshoot git command failures.

446-463: ⚖️ Poor tradeoff

Potential race condition in concurrent teardown (edge case).

Between checking _session_closed (line 453) and setting it (line 458), concurrent calls could both pass the check and attempt cleanup twice. This is unlikely in the Hermes lifecycle (atexit, on_session_end, and shutdown are typically sequential), but could theoretically happen if shutdown is called from multiple threads.

If you want to eliminate the race, use a threading lock. However, given the current usage patterns, this is likely acceptable as-is.

🔒 Optional: Add thread-safe guard
+import threading
+
 class AgentMemoryProvider(MemoryProvider):
+    def __init__(self):
+        self._close_lock = threading.Lock()
 
     def _close_session(self, session_id: str | None = None) -> None:
         """Idempotently end the agentmemory session.
 
         Every teardown path converges here - on_session_end()/shutdown() on
         the cli/gateway paths and the atexit hook on the ACP/headless path -
         so the session is ended exactly once and never left dangling.
         """
-        if getattr(self, "_session_closed", False):
-            return
+        with self._close_lock:
+            if getattr(self, "_session_closed", False):
+                return
+            self._session_closed = True
         sid = session_id or getattr(self, "_session_id", None)
         if not sid or not getattr(self, "_base", None):
             return
-        self._session_closed = True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/hermes/__init__.py` around lines 446 - 463, The _close_session
method has a small race between checking and setting _session_closed; make it
thread-safe by introducing a lock (e.g., an instance threading.Lock or
threading.RLock stored as self._session_lock in the initializer) and wrap the
check-and-set and early-return logic in a with self._session_lock: block inside
_close_session so only one thread can pass the guard and set
self._session_closed = True before proceeding to atexit.unregister and calling
_api(self._base, "session/end", {"sessionId": sid}).

72-72: 💤 Low value

Remove unnecessary sorted() call.

The order of candidates doesn't affect the logic, so sorting adds unnecessary overhead. When scanning directories with many entries, this could be noticeable.

♻️ Suggested change
-    for name in sorted(os.listdir(cwd)):
+    for name in os.listdir(cwd):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/hermes/__init__.py` at line 72, The loop currently iterates
using sorted(os.listdir(cwd)), causing unnecessary sorting overhead; change the
iteration to use os.listdir(cwd) directly (or use os.scandir(cwd) for better
performance on large directories) by removing the sorted() wrapper where the
loop over directory entries is formed (the for name in ... loop that references
cwd).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@integrations/hermes/__init__.py`:
- Line 263: The assignment to self._cwd uses "or" which treats an explicit empty
string as false and falls back to os.getcwd(); change the logic in the __init__
so you only fall back when cwd is absent/None (not when it's the empty string)
by checking kwargs["cwd"] or kwargs.get("cwd") for None explicitly and assigning
that value to self._cwd otherwise using os.getcwd(); update the expression
around self._cwd, referencing kwargs["cwd"], kwargs.get("cwd"), and os.getcwd()
accordingly so an explicit "" is preserved.
- Around line 269-273: The session/start API call made via _api(self._base,
"session/start", {...}) is not checked; wrap the call in error handling and
verify its success (catch exceptions from _api or inspect its returned result),
then on failure either raise a clear exception or log an error and abort/return
early so the plugin does not continue with an unstarted session; reference the
session_id, self._project and self._cwd parameters in the error message to aid
debugging.

---

Nitpick comments:
In `@integrations/hermes/__init__.py`:
- Around line 26-45: The _project_from_remote function swallows all exceptions
and returns an empty string, making failures hard to diagnose; update the except
block in _project_from_remote to optionally log the caught exception (e.g.,
using the module logger via logging.getLogger(__name__)) at debug or warning
level with context like "git remote get-url origin failed for path=%s" before
returning "", so callers still get graceful degradation but developers can
enable logs to troubleshoot git command failures.
- Around line 446-463: The _close_session method has a small race between
checking and setting _session_closed; make it thread-safe by introducing a lock
(e.g., an instance threading.Lock or threading.RLock stored as
self._session_lock in the initializer) and wrap the check-and-set and
early-return logic in a with self._session_lock: block inside _close_session so
only one thread can pass the guard and set self._session_closed = True before
proceeding to atexit.unregister and calling _api(self._base, "session/end",
{"sessionId": sid}).
- Line 72: The loop currently iterates using sorted(os.listdir(cwd)), causing
unnecessary sorting overhead; change the iteration to use os.listdir(cwd)
directly (or use os.scandir(cwd) for better performance on large directories) by
removing the sorted() wrapper where the loop over directory entries is formed
(the for name in ... loop that references cwd).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1604e08b-1ca6-40bb-9ed0-a1f7b48d625f

📥 Commits

Reviewing files that changed from the base of the PR and between 25e7701 and 8966e05.

📒 Files selected for processing (1)
  • integrations/hermes/__init__.py

Comment thread integrations/hermes/__init__.py
Comment thread integrations/hermes/__init__.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant