fix(gateway): run exec quick commands inline when agent is mid-turn (#25783)#25804
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Extracts exec-type quick command execution into a reusable helper so that quick commands can bypass the busy-session queue/interrupt path and run inline during an active agent turn (fixes #25783).
Changes:
- Adds
_try_execute_exec_quick_commandhelper consolidating exec subprocess logic with env sanitization, timeout, and redaction. - Inserts a short-circuit in
_handle_active_session_busy_message(after the draining branch) so exec quick commands run inline and reply without touching the running agent. - Refactors the cold-path quick command branch to delegate to the new helper, plus adds a new test module covering busy bypass, drain precedence, alias fall-through, and helper edge cases.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| gateway/run.py | Extracts exec quick command runner; short-circuits busy-session path for exec commands. |
| tests/gateway/test_busy_quick_command_bypass.py | New tests verifying busy bypass, drain precedence, alias fall-through, and helper behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| exec_cmd = qcmd.get("command", "") | ||
| if not exec_cmd: | ||
| return f"Quick command '/{command}' has no command defined." |
| reply_to=( | ||
| reply_anchor | ||
| if event.source.platform == Platform.TELEGRAM | ||
| and event.source.chat_type == "dm" | ||
| and event.source.thread_id | ||
| else (None if event.source.platform == Platform.TELEGRAM and event.source.thread_id else event.message_id) | ||
| ), |
| try: | ||
| # Sanitize env to prevent credential leakage — quick commands run | ||
| # in the gateway process which has all API keys in os.environ. | ||
| from tools.environments.local import _sanitize_subprocess_env |
| with patch("gateway.run.asyncio.create_subprocess_shell", new=AsyncMock(return_value=fake_proc)) as mock_spawn, \ | ||
| patch("gateway.run.merge_pending_message_event") as mock_merge: | ||
| result = await runner._handle_active_session_busy_message(event, sk) |
| event = _make_event(text="/note while draining") | ||
| sk = build_session_key(event.source) | ||
| runner.adapters[event.source.platform] = adapter | ||
|
|
||
| with patch("gateway.run.asyncio.create_subprocess_shell", new=AsyncMock()) as mock_spawn: | ||
| result = await runner._handle_active_session_busy_message(event, sk) |
|
@copilot Addressed in 49b06ec51:
Deliberately not changing in this PR:
All 6 tests in |
|
CI audit — all 46 test failures in the latest Tests run are pre-existing baselines on clean
All six focused tests in |
49b06ec to
d9fb3ea
Compare
d9fb3ea to
634a3f6
Compare
634a3f6 to
f9640a5
Compare
f9640a5 to
38606b2
Compare
…ousResearch#25783) Adapters route incoming messages to ``_handle_active_session_busy_message`` whenever the agent is mid-turn, but that path only knew how to queue/interrupt/steer. Exec-type quick commands (``quick_commands.<name>.type: exec``) bypass the LLM by design — they spawn a subprocess and return its output. When a user sent ``/note ...`` during a multi-tool-call turn, the busy handler had no knowledge of quick commands, so the message was ``merge_pending_message_event``'d into the pending queue and replayed to the agent as raw text on the next turn. Net effect: the daily-note script never ran, and the agent saw ``/note remember to buy milk`` as a user message. Extract the existing exec dispatch + execution from ``_handle_message`` into ``_try_execute_exec_quick_command`` so both the cold path and the busy path can call it. In the busy handler, run the helper after the draining branch (matching the precedence in ``_handle_message``: draining wins, exec quick commands next) and deliver the output via ``adapter._send_with_retry``. ``alias`` quick commands intentionally are NOT short-circuited — they rewrite into agent input and follow normal busy semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…olation Addresses Copilot inline feedback on NousResearch#25804: * Extract `_busy_ack_reply_to(event, reply_anchor)` to centralize the three-way Telegram reply-target rule that was duplicated across the drain, exec-bypass, and normal busy-ack send sites in `_handle_active_session_busy_message`. The behaviour is unchanged — the goal is just preventing the three branches from drifting on future Telegram changes. * Patch the helper's lazy cross-module imports (`tools.environments.local._sanitize_subprocess_env`, `agent.redact.redact_sensitive_text`) inside the busy-bypass test so it stays hermetic with the `telegram` stub already at the top of the file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
38606b2 to
ee3dc72
Compare
|
Closing to focus the queue on security/file-safety work where civilian merges are landing. Happy to reopen if maintainers want this picked up. |
Summary
The bug
`gateway/run.py` registers `_handle_active_session_busy_message` as each adapter's `busy_session_handler`. Whenever a message arrives for an active session that's mid-turn, the adapter routes through that handler instead of the cold path `_handle_message` — so the existing exec dispatch at `gateway/run.py:6582` was unreachable. The busy handler only knew queue / interrupt / steer:
```
user sends `/note remember to buy milk` during a tool-call turn
→ adapter detects active session
→ calls _handle_active_session_busy_message
→ merge_pending_message_event(...) # queued
→ next turn: agent sees "/note remember to buy milk" as user text
```
The user's daily-note script never runs; the agent receives the slash command as conversation input.
The fix
Test plan
Related