fix(gateway): unblock update subprocess on recognized-command bypass

When the gateway intercepts a pending /update prompt and the user sends
a recognized slash command (/new, /help, ...), the command now dispatches
normally AND the detached update subprocess is unblocked by writing a
blank .update_response. _gateway_prompt reads '' → strips → returns the
prompt's default (typically a safe 'n' / skip), so the update process
exits cleanly instead of blocking on stdin until the 30-minute watcher
timeout.

Also clears _update_prompt_pending[session_key] on this path so stray
future input for the same session isn't re-intercepted.

Extends PR #15849 with tests for the new cancel-write + a regression
test pinning the legacy behavior of unrecognized /foo slash commands
still being consumed as the response.
This commit is contained in:
Teknium
2026-04-26 18:33:55 -07:00
committed by Teknium
parent bdaf56a94d
commit 90c84c6dba
2 changed files with 62 additions and 3 deletions

View File

@@ -3469,6 +3469,30 @@ class GatewayRunner:
_update_prompts.pop(_quick_key, None)
label = response_text if len(response_text) <= 20 else response_text[:20] + ""
return f"✓ Sent `{label}` to the update process."
# Recognized slash command during a pending update prompt:
# unblock the detached update subprocess by writing a blank
# response so ``_gateway_prompt`` returns the prompt's default
# (typically a safe "n" / skip) and exits cleanly instead of
# blocking on stdin until the 30-minute watcher timeout.
# The slash command then falls through to normal dispatch.
if _recognized_cmd:
response_path = _hermes_home / ".update_response"
try:
tmp = response_path.with_suffix(".tmp")
tmp.write_text("")
tmp.replace(response_path)
logger.info(
"Recognized /%s during pending update prompt for %s; "
"cancelled prompt with default and dispatching command",
_recognized_cmd,
_quick_key,
)
except OSError as e:
logger.warning(
"Failed to write cancel response for pending update prompt: %s",
e,
)
_update_prompts.pop(_quick_key, None)
# PRIORITY handling when an agent is already running for this session.
# Default behavior is to interrupt immediately so user text/stop messages

View File

@@ -491,7 +491,13 @@ class TestUpdatePromptInterception:
@pytest.mark.asyncio
async def test_recognized_slash_command_bypasses_pending_update_prompt(self, tmp_path):
"""Known slash commands must dispatch normally instead of being consumed."""
"""Known slash commands must dispatch normally instead of being consumed.
The update subprocess is still blocked on stdin waiting for
``.update_response``, so the gateway writes a blank response to
unblock it (``_gateway_prompt`` returns the prompt's default on
empty) before falling through to normal command dispatch.
"""
runner = _make_runner()
hermes_home = tmp_path / "hermes"
hermes_home.mkdir()
@@ -508,8 +514,37 @@ class TestUpdatePromptInterception:
assert result == "reset ok"
runner._handle_reset_command.assert_awaited_once_with(event)
assert not (hermes_home / ".update_response").exists()
assert runner._update_prompt_pending[session_key] is True
# .update_response was written (empty) to unblock the update
# subprocess; _gateway_prompt will read "", strip to "", and
# return the prompt's default.
response_path = hermes_home / ".update_response"
assert response_path.exists()
assert response_path.read_text() == ""
# Pending flag is cleared so stray future input won't be
# re-intercepted for a prompt that is no longer outstanding.
assert session_key not in runner._update_prompt_pending
@pytest.mark.asyncio
async def test_unrecognized_slash_command_still_consumed_as_response(self, tmp_path):
"""Unknown /foo is written verbatim to .update_response (legacy behavior)."""
runner = _make_runner()
hermes_home = tmp_path / "hermes"
hermes_home.mkdir()
event = _make_event(text="/foobarbaz", chat_id="67890")
session_key = "agent:main:telegram:dm:67890"
runner._update_prompt_pending[session_key] = True
runner._is_user_authorized = MagicMock(return_value=True)
runner._session_key_for_source = MagicMock(return_value=session_key)
with patch("gateway.run._hermes_home", hermes_home):
result = await runner._handle_message(event)
response_path = hermes_home / ".update_response"
assert response_path.exists()
assert response_path.read_text() == "/foobarbaz"
assert "Sent" in (result or "")
assert session_key not in runner._update_prompt_pending
@pytest.mark.asyncio
async def test_normal_message_when_no_prompt_pending(self, tmp_path):