fix(bot-mode): resume nested one-shot DMs on the recipient's session

Quiet chat -Q inherited the dispatcher's HERMES_SESSION_KEY, so a nested
message_agent notify was addressed to the grandparent and B never woke.
Bind this session's key, strip inherited session identity from the delivery
child env, and continue owned notify completions in-process before stdout.
This commit is contained in:
xxxigm
2026-09-14 20:11:57 +07:00
committed by kshitij
parent 5c26cf64b2
commit 9a6c7a9314
2 changed files with 54 additions and 13 deletions
+47 -12
View File
@@ -4073,24 +4073,59 @@ def _sync_cli_session_id_from_agent(cli) -> None:
def _run_quiet_single_query(cli, effective_query):
"""Quiet (-Q) one-shot turn: run, print the response (stderr for errors/session_id), then sys.exit with the automation exit code.
HERMES_TURN_AUTHOR (set only by a bot-to-bot dispatcher) is consumed here so tool subprocesses do not inherit it."""
HERMES_TURN_AUTHOR (set only by a bot-to-bot dispatcher) is consumed here so tool subprocesses do not inherit it.
Nested Bot Mode notifies bind this session's key (not the dispatcher's) and resume in-process
before stdout is printed, so a teammate reply is the quiet run's final answer rather than a
stranded receipt."""
from agent.interrupt_compat import _accepts_keyword
from agent.turn_author import take_turn_author_from_env
from hermes_cli.quiet_single_query import bind_quiet_session_key, continue_quiet_notify_completions
author = take_turn_author_from_env()
author_kwargs = {"turn_author": author} if author is not None and _accepts_keyword(cli.agent.run_conversation, "turn_author") else {}
key_token, reset_session_key = None, None
try:
result = cli.agent.run_conversation(
user_message=effective_query, conversation_history=cli.conversation_history, **author_kwargs,
)
except KeyboardInterrupt:
_emit_interrupted_session_end(cli, reason="keyboard_interrupt")
print(f"\nsession_id: {cli.session_id}", file=sys.stderr)
sys.exit(130)
# The exit line below reports session_id to stderr for automation wrappers;
# without this sync it would point at the ended parent after compression.
_sync_cli_session_id_from_agent(cli)
response = result.get("final_response", "") if isinstance(result, dict) else str(result)
key_token, reset_session_key = bind_quiet_session_key(getattr(cli, "session_id", "") or "default")
except Exception:
key_token, reset_session_key = None, None
try:
try:
result = cli.agent.run_conversation(
user_message=effective_query, conversation_history=cli.conversation_history, **author_kwargs,
)
except KeyboardInterrupt:
_emit_interrupted_session_end(cli, reason="keyboard_interrupt")
print(f"\nsession_id: {cli.session_id}", file=sys.stderr)
sys.exit(130)
# The exit line below reports session_id to stderr for automation wrappers;
# without this sync it would point at the ended parent after compression.
_sync_cli_session_id_from_agent(cli)
if isinstance(result, dict) and not result.get("failed"):
history = result.get("messages") or cli.conversation_history
def _follow_up(text):
nonlocal history
follow = cli.agent.run_conversation(
user_message=text, conversation_history=history, **author_kwargs,
)
if isinstance(follow, dict) and follow.get("messages"):
history = follow["messages"]
return follow
continued = continue_quiet_notify_completions(
getattr(cli, "session_id", "") or "",
_follow_up,
owns_event=getattr(cli, "_owns_process_notification", None),
)
if isinstance(continued, dict):
result = continued
response = result.get("final_response", "") if isinstance(result, dict) else str(result)
finally:
if key_token is not None and reset_session_key is not None:
try:
reset_session_key(key_token)
except Exception:
pass
# Surface backend errors that produced no visible output (e.g. invalid model slug
# -> provider 4xx) on stderr so piped stdout stays clean.
if (