Skip to content

fix(context-engine): clone plugin engines per agent - #42683

Closed
the3asic wants to merge 1 commit into
NousResearch:mainfrom
the3asic:fix/context-engine-per-agent-clone
Closed

fix(context-engine): clone plugin engines per agent#42683
the3asic wants to merge 1 commit into
NousResearch:mainfrom
the3asic:fix/context-engine-per-agent-clone

Conversation

@the3asic

@the3asic the3asic commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add an optional ContextEngine.clone_for_agent() hook for plugin context engines.
  • Call the hook during AIAgent initialization before model/session binding.
  • Track ownership so AIAgent.close() only shuts down cloned per-agent engines, not process-wide registered singletons.
  • Add host-contract regressions for cloned vs shared plugin engines.

Why

Plugin context engines are registered process-wide, but gateway runtimes can keep multiple cached AIAgent instances alive at once (different chats, platforms, cron jobs, etc.). Engines with mutable session binding or ingest cursor state need a per-agent runtime instance; otherwise one cached agent can rebind another agent's context engine state.

The default hook returns self, preserving existing shared-instance behavior for stateless engines and old plugins.

Pairs with stephenschoettler/hermes-lcm#247, which implements the hook for LCM.

Test plan

  • python -m pytest tests/agent/test_context_engine_host_contract.py tests/agent/test_context_engine.py tests/run_agent/test_compression_boundary_hook.py -q -o 'addopts='

@liuhao1024

Copy link
Copy Markdown
Contributor

Positive verification — context engine per-agent cloning

Reviewed the diff across agent/agent_init.py, agent/context_engine.py, run_agent.py, and tests.

Problem: Plugin context engines are registered process-wide, but gateway runtimes may keep multiple cached AIAgent instances alive simultaneously (different platforms, chats, cron jobs). Engines that maintain mutable session state (cursors, bindings) on self would share that state across agents — causing cross-session contamination.

Fix: clone_for_agent() method on ContextEngine (default returns self for backward compatibility). init_agent() calls it before binding session state; if the clone is a different object, the agent sets _owns_context_engine = True and takes ownership of its lifecycle. AIAgent.close() calls the shutdown method only on owned engines, leaving shared registered singletons alive for other agents.

Test coverage:

  • test_agent_init_clones_plugin_context_engine_per_agent verifies clone is created, agent owns it, update_model/on_session_start are called on the clone (not the registered instance), and close() tears down the clone but not the registered engine
  • test_agent_close_does_not_shutdown_shared_plugin_context_engine verifies the default clone_for_agent() (returns self) preserves backward-compatible shared behavior

The error handling in init_agent() gracefully falls back to the registered instance if clone_for_agent() raises or returns None.

LGTM. ✅

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/plugins Plugin system and bundled plugins labels Jun 9, 2026
@stephenschoettler

Copy link
Copy Markdown
Contributor

Downstream validation from the hermes-lcm side: this is the host-side piece for stephenschoettler/hermes-lcm#243 / stephenschoettler/hermes-lcm#247.

I checked current head 2b2a590 against current NousResearch/hermes-agent main 4829f8d.

Local validation:

  • exact PR head: python -m pytest tests/agent/test_context_engine_host_contract.py tests/agent/test_context_engine.py tests/run_agent/test_compression_boundary_hook.py -q -o 'addopts=' -> 34 passed
  • local no-commit merge of current origin/main into this PR head: same command -> 34 passed
  • python -m py_compile agent/agent_init.py agent/context_engine.py run_agent.py tests/agent/test_context_engine_host_contract.py -> passed

Current main workflows are green. GitHub still shows no checks reported for this fork branch, so the remaining blocker looks like CI/review routing rather than a failing downstream smoke.

Phantomthedog added a commit to Phantomthedog/hermes-agent that referenced this pull request Jun 15, 2026
Local backport of upstream PR NousResearch#42683
(commit 2b2a590). Adapted for our codebase.

Adds ContextEngine.clone_for_agent() and shutdown(), per-agent
engine isolation in AIAgent init and close, and 9 new tests.

Related: stephenschoettler/hermes-lcm#243, stephenschoettler/hermes-lcm#247
@qxxaa

qxxaa commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Tested on my environment running multiple concurrent agents (WebUI session + background cron jobs + subagents) against the same hermes-agent process.

Before the fix, background tasks calling on_session_start() on the shared context engine singleton would overwrite the main session's binding. When LCM compression fired on the main conversation, DAG nodes were attributed to the phantom background session ID and became unreachable from the main session (lcm_expand returned nothing).

After applying the clone fix, each agent gets its own engine instance. Compression now correctly attributes DAG nodes to the originating session. Verified by running lcm_expand on post-compression nodes - all resolve cleanly with the correct session ID.

@stephenschoettler

Copy link
Copy Markdown
Contributor

Superseded by #62374, rebuilt on current main. This replacement preserves and credits the per-agent clone contract direction from this PR while incorporating the deepcopy baseline and current agent lifecycle.

1 similar comment
@stephenschoettler

Copy link
Copy Markdown
Contributor

Superseded by #62374, rebuilt on current main. This replacement preserves and credits the per-agent clone contract direction from this PR while incorporating the deepcopy baseline and current agent lifecycle.

@teknium1 teknium1 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.

Thanks for identifying the process-wide context-engine lifecycle issue. The current branch cannot be applied safely to current main as written.

Problems

  • agent/context_engine.py:120 makes the default clone_for_agent() return self. The initialization change then accepts that alias, while current main explicitly uses copy.deepcopy(_candidate) at agent/agent_init.py:1785-1805 to prevent a child agent's model/session binding from mutating the registered template (#42449).
  • The branch tears down an owned clone only in AIAgent.close(). The replacement implementation in #62374 also handles the soft release_clients() lifecycle, which is needed when cached agents are evicted without a hard close.

Suggested changes

  • Please use the rebuilt #62374 approach: preserve deepcopy as the default clone contract, reject aliases of the registered template, and centralize idempotent teardown for both soft release and close.

This is an automated hermes-sweeper review.

Comment thread agent/context_engine.py
platforms, chats, cron jobs, etc.). Engines that keep mutable session
binding or cursor state on ``self`` should override this method and
return a fresh engine instance that shares durable storage/configuration
as needed. Stateless engines can use the default, which preserves the

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.

Returning the registered template here regresses current main's deepcopy isolation: init_agent subsequently calls update_model() and on_session_start() on this object. Default behavior must create an isolated copy (and init must reject template aliases), as in the rebuilt #62374 approach.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
@alt-glitch alt-glitch removed the sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades label Jul 14, 2026
@the3asic

Copy link
Copy Markdown
Contributor Author

Closing as superseded by #62374, which rebuilds this fix on current main and preserves credit for the original direction. Thanks for carrying it forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants