fix(gateway): emit agent:end on interrupt/drain follow-up turns - #38915
Closed
Couiz wants to merge 3 commits into
Closed
fix(gateway): emit agent:end on interrupt/drain follow-up turns#38915Couiz wants to merge 3 commits into
Couiz wants to merge 3 commits into
Conversation
The main message dispatch emits the `agent:start` hook before running the agent, but the interrupt/drain follow-up path in `_run_agent` promoted a queued message straight into a recursive `_run_agent` call without emitting `agent:start`. Every hook listening on `agent:start` (SessionStart-style integrations, activity loggers, visualizers) silently missed interrupt/queue follow-up turns — an event-emission gap, not a hook bug. Emit `agent:start` on the drain path right before the recursive `_run_agent`, mirroring the main-dispatch payload (platform, user_id, chat_id, session_id, message[:500]) but built from the follow-up turn's source (`next_source`) and the final, already-transcribed text (`next_message`) — so voice follow-ups carry the transcript, not the raw audio placeholder. The emit sits after every discard guard (draining, interrupt depth-cap, stale /goal continuation, transcription→None) so it fires exactly once per turn, only when the follow-up actually proceeds to the agent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…depth
Both agent:start emit sites — the main inbound dispatch and the
interrupt/drain follow-up path in _run_agent — previously emitted an
identical payload shape, so hooks (voice-echo, activity loggers,
visualizers) could not tell a fresh user turn from an interrupt-driven
follow-up turn.
Add two discriminator fields to the agent:start payload at both sites:
* trigger — a string, not a bool, so future turn kinds like "goal" or
"schedule" can be added without breaking the contract: "message" on the
main dispatch, "interrupt" on the drain follow-up. Hooks read it
backward-compatibly as context.get("trigger", "message").
* interrupt_depth — an int: 0 on the main dispatch (a fresh turn is never
an interrupt); _interrupt_depth + 1 on the drain path, matching the
depth handed to the recursive _run_agent call (first interrupt -> 1,
interrupt-of-an-interrupt -> 2, ...).
Both payloads stay shape-consistent (7 keys); no existing key changes. The
drain emit still sits after every discard guard, so it fires once per turn
only when the follow-up actually proceeds to the agent.
Depends on NousResearch#37269.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
|
Consolidated into #39126 — the three changes (agent:start on drain, trigger/depth tagging, agent:end on drain) are one coherent concern, and the agent:end fix's payload carries the trigger/interrupt_depth tagging, so they aren't cleanly separable. A single PR off fresh main reviews better than three stacked PRs across diverging bases. Closing in favor of #39126. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On an interrupted/drained turn the gateway emits
agent:startfor the drained follow-up (added in the prior fix that introduced the drainagent:startemit) but never emits a matchingagent:end. The onlyagent:endemit lives in the outer_handle_message_with_agentpath (main dispatch); the drained follow-up re-enters_run_agentrecursively, not that outer caller, so its end event is silently dropped. Hooks that pairagent:start/agent:end(SessionStart-style integrations, activity loggers, turn visualizers) therefore see one unmatchedstarton every interrupt.Fix (minimal, symmetric)
In the drain path of
_run_agent, the existingagent:startpayload is now built once asfollowup_hook_ctxand reused for a pairedagent:endemitted right after the recursive follow-up completes — mirroring exactly how the main path reuseshook_ctxfor both its start (line ~9287) and end (~9428) emits. The end carries the sametrigger="interrupt"/interrupt_depthtagging so it's attributable to the same drained turn, and itsresponseis the follow-up'sfinal_response[:500](same shape as the main-path end).The emit sits below the discard and
_MAX_INTERRUPT_DEPTHearly-returns, so paths that emit nostartalso emit noend— symmetry is preserved on every branch (verified by a max-depth boundary test and the two discard tests).Verification (empirical first)
A characterization pass confirmed the asymmetry is real before any production change: the drain path emitted
startwith no matchingend. The regression suite (tests/gateway/test_drain_agent_end_symmetry.py, 6 tests) covers: start/end balanced on a single interrupt, end payload mirrors start, nested-depth end incrementsinterrupt_depth, no end at_MAX_INTERRUPT_DEPTH, and the two discard paths emit neither. RED against the pre-fix code (end count < start count), GREEN after. Fulltests/gateway/suite shows zero new failures attributable to this change (the only diff vs. base is these 3 fix-dependent tests flipping RED→GREEN).Stacking
Depends on #37269 (emit
agent:starton interrupt/drain) and #37291 (tagagent:startpayload withtrigger/interrupt_depth). This PR is based on the #37291 branch and completes the start/end symmetry that #37269 began. Please merge those first.