mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
fix: every /bg entry point joins its background delegations in the run
The gateway /bg fix above covered messaging chats only. The shared-owner /bg (prompt.background, used by the CLI, TUI and Desktop) and the classic local CLI /bg run the same detached bg_* side agent, and on main a background delegate_task there also leaves a completed ledger row pending forever: nothing wakes or drains a bg_* session. Their side agents now declare the channel stateless, so the batch runs inline and the result is in the one answer /bg reports. The sync-fallback note names /bg. Refs #140569. Co-authored-by: milnerrad <302432023+milnerrad@users.noreply.github.com>
This commit is contained in:
1 parent
3d26ec8ee2
commit
2d0bf91f11
4 files changed
+61
-7
No files matched your search
@@ -66,7 +66,10 @@ def _side_scope(authority, policy, task_id):
|
||||
home = authority.profile_id
|
||||
profile = served_profile_name(home) if _is_routed_home(home) else ''
|
||||
with owner_scope(authority, hydrate_secrets=True), policy_scope(policy, authority=authority):
|
||||
tokens = set_session_vars(session_key=task_id, session_id=task_id, cwd=policy.cwd, profile=profile)
|
||||
# async_delivery=False: the side agent's answer is one ``*.complete`` event, and its bg_* session
|
||||
# has no later turn to wake, so a background delegate_task joins in this run (#140569).
|
||||
tokens = set_session_vars(session_key=task_id, session_id=task_id, cwd=policy.cwd, profile=profile,
|
||||
async_delivery=False)
|
||||
try:
|
||||
# The session's workspace, as its own turns get it (#140665): mounted at /workspace in a
|
||||
# per-task container, or the session's own persistent workspace container (same digest).
|
||||
|
||||
@@ -1362,6 +1362,8 @@ class CLICommandsMixin(CLICommandsSessionToolsMixin):
|
||||
|
||||
def produce():
|
||||
from agent.vault_backends.unlock import set_code_prompt_callback, set_save_login_prompt_callback, set_unlock_prompt_callback
|
||||
from gateway.session_context import declare_stateless_channel
|
||||
declare_stateless_channel() # nothing drains a bg_* session's completions later: delegations join (#140569)
|
||||
set_sudo_password_callback(self._sudo_password_callback)
|
||||
set_approval_callback(self._approval_callback)
|
||||
set_unlock_prompt_callback(self._vault_unlock_callback)
|
||||
@@ -1402,10 +1404,8 @@ class CLICommandsMixin(CLICommandsSessionToolsMixin):
|
||||
response = _gt("model.error_prefix", error=result["error"])
|
||||
return response
|
||||
finally:
|
||||
# One agent per /bg task in a long-lived CLI process: close()
|
||||
# is the owner boundary (memory shutdown, tool subprocesses,
|
||||
# httpx clients); an unclosed side agent leaks all of them
|
||||
# until the CLI exits (#50197).
|
||||
# One agent per /bg task in a long-lived CLI: close() is the owner boundary (memory,
|
||||
# tool subprocesses, httpx clients); unclosed, they leak until the CLI exits (#50197).
|
||||
with suppress(Exception):
|
||||
bg_agent.close()
|
||||
finally:
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Classic-CLI /bg: a background delegate_task joins inside the bg_* run (#140569).
|
||||
|
||||
The /bg answer is printed once and nothing drains completions for its bg_* session, so a detached
|
||||
child's result would sit in the ledger forever.
|
||||
"""
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import cli as cli_module
|
||||
from tests.hermes_cli.test_cli_approval_ui import _make_background_cli_stub
|
||||
|
||||
|
||||
def test_background_task_delegation_runs_inline(monkeypatch):
|
||||
import tools.delegate_tool as dt
|
||||
|
||||
monkeypatch.setattr(dt, "_build_child_agent", lambda **kw: SimpleNamespace(_subagent_id="child"))
|
||||
monkeypatch.setattr(dt, "_resolve_delegation_credentials", lambda *a, **kw: {
|
||||
"model": "test", "provider": None, "base_url": None, "api_key": None,
|
||||
"api_mode": None, "command": None, "args": None,
|
||||
})
|
||||
monkeypatch.setattr(dt, "_run_single_child", lambda **kw: {
|
||||
"task_index": 0, "status": "completed", "summary": "child work product",
|
||||
"api_calls": 1, "duration_seconds": 0, "model": "test", "exit_reason": "completed",
|
||||
})
|
||||
detached = MagicMock(return_value={"delegation_id": "detached"})
|
||||
monkeypatch.setattr("tools.async_delegation.dispatch_async_delegation_batch", detached)
|
||||
seen = {}
|
||||
|
||||
class FakeAgent:
|
||||
def __init__(self, **kwargs):
|
||||
self._print_fn = None
|
||||
self.thinking_callback = None
|
||||
|
||||
def run_conversation(self, **kwargs):
|
||||
parent = SimpleNamespace(_delegate_depth=0, _subagent_id=None)
|
||||
seen["tool"] = dt.delegate_task(goal="inspect workspace", background=True, parent_agent=parent)
|
||||
return {"final_response": "done", "messages": []}
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
cli = _make_background_cli_stub()
|
||||
with patch("run_agent.AIAgent", FakeAgent), patch.object(cli_module, "_cprint"), \
|
||||
patch.object(cli_module, "ChatConsole"):
|
||||
cli._handle_background_command("/bg inspect workspace")
|
||||
for thread in list(cli._background_tasks.values()):
|
||||
thread.join(timeout=10)
|
||||
|
||||
detached.assert_not_called()
|
||||
assert "child work product" in json.dumps(seen["tool"])
|
||||
@@ -229,8 +229,8 @@ _SYNC_FALLBACK_NOTES = {
|
||||
"no_async": (
|
||||
"background=true is not available in this session — it cannot "
|
||||
"receive a detached subagent result after the turn ends (a "
|
||||
"finite chat using -Q, --oneshot, or non-TTY stdio, `hermes -z`, a cron job, a Kanban "
|
||||
"worker, or a stateless HTTP endpoint). The subagent(s) ran SYNCHRONOUSLY and the result is included above."
|
||||
"finite chat using -Q, --oneshot, or non-TTY stdio, `hermes -z`, a /bg background task, a cron "
|
||||
"job, a Kanban worker, or a stateless HTTP endpoint). The subagent(s) ran SYNCHRONOUSLY and the result is included above."
|
||||
),
|
||||
"at_capacity": (
|
||||
"The background delegation pool was at capacity (delegation.max_concurrent_children), so the subagent(s) ran "
|
||||
|
||||
Reference in new issue
Block a user