agentHost: add generic agent turn hang telemetry - #329884
Merged
Merged
Conversation
A hung agent turn was previously only detectable as the *absence* of
`agentHost.turnCompleted` after `agentHost.userMessageSent`, which does
not surface on dashboards. Add a positive signal.
`AgentHostTurnTracker` now arms a `disposableTimeout` watchdog per
in-flight turn, debounced by any turn-scoped activity. On expiry it
classifies the quiet state and reports `agentHost.turnHung`:
unexpected: `noProgress` (nothing was ever observed for the turn - the
signature of a lost turn), `stalledAfterProgress`
expected: `waitingOnUser` (an input request is outstanding),
`runningTool` (a tool call is in flight, which also covers
subagents)
Each reason is reported at most once per turn and the watchdog re-arms
for at most six quiet windows, so a permanently dead turn cannot produce
unbounded events or timers. A paired `agentHost.hungTurnCompleted` fires
when a previously hung turn later completes, so permanent hangs can be
separated from merely slow ones.
`AgentSideEffects` feeds the watchdog: turn-scoped protocol actions as
activity, tool call start/complete as in-flight state, and session input
requests as blockers. Turn tracking is also cleared on channel teardown
and on truncation, closing a pre-existing leak where entries for turns
that never completed lived forever.
The existing `agentHost.toolCallStalled` telemetry is kept as-is: it is a
more specialized signal (it carries tool identity and only covers tool
blockers), and keeping it avoids breaking existing queries.
(Written by Copilot)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds positive telemetry for agent turns that become inactive, including hang classification and recovery reporting.
Changes:
- Adds debounced per-turn hang watchdogs and cleanup.
- Adds hang/recovery telemetry events and wiring.
- Adds integration coverage for hang scenarios.
Show a summary per file
| File | Description |
|---|---|
agentHostTurnHangTelemetry.test.ts |
Tests watchdog behavior and cleanup. |
agentSideEffects.ts |
Wires activity, blockers, tools, and teardown. |
agentService.ts |
Uses expanded telemetry cleanup. |
agentHostTurnTracker.ts |
Implements hang tracking and classification. |
agentHostTelemetryReporter.ts |
Defines and emits new telemetry events. |
Review details
Tip
Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Balanced
Two fixes from PR review, both cases where the watchdog would misstate what a turn was waiting for. Client tool execution is not a wait on the user. The protocol defines `SessionInputRequestKind.ToolClientExecution` as delegated running work rather than a prompt — the call has already cleared its confirmation gate and is simply executing elsewhere — and the session reducer's `awaitsUser` predicate excludes it for exactly this reason. Recording it as a user blocker classified every long-running client tool as `waitingOnUser` with `blockedOn: toolClientExecution`. Outstanding requests are still tracked for bookkeeping, but only user-blocking kinds drive `waitingOnUser` and `blockedOn`; client execution is represented by the in-flight tool set and so reports `runningTool`. That predicate is mirrored rather than imported because the reducer file is generated. A denied tool confirmation is terminal. The reducer moves the call to `cancelled` and ignores any later completion for it, so the tool call id stayed in the turn's in-flight set forever and a subsequent hang was reported as an expected `runningTool` instead of `stalledAfterProgress`. Denials now clear the entry on both the client (`handleAction`) and agent (`_dispatchActionForSession`) paths, since the action arrives on both. Both new tests were verified to fail against the pre-fix code. (Written by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
roblourens
marked this pull request as ready for review
August 10, 2026 04:26
roblourens
enabled auto-merge (squash)
August 10, 2026 04:26
Don Jayamanne (DonJayamanne)
approved these changes
Aug 10, 2026
Don Jayamanne (DonJayamanne)
approved these changes
Aug 10, 2026
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.
Why
Today a hung agent turn is only detectable as the absence of telemetry — an
agentHost.userMessageSentwith no matchingagentHost.turnCompleted. Absences don't show up on dashboards, which is why several session-hang bugs shipped undetected. This adds a positive signal.The motivating case: a session config change caused the Agent Host to destroy its Copilot SDK session and immediately resume the same session ID; a runtime race then dropped the resumed session's native callback registration.
session.send()returned normally, but nouser.messagewas persisted, no model turn ever started, and no error or terminal event was ever emitted.turnStartedfired;turnCompletednever did. The UI sat on "Working…" forever. This change surfaces exactly that ashangReason: 'noProgress'/hadAnyProgress: false.What
AgentHostTurnTrackerarms adisposableTimeoutwatchdog per in-flight turn, debounced by activity — any turn-scoped protocol action re-arms it. On expiry it classifies the quiet state and emitsagentHost.turnHung.The threshold is
5 * 60 * 1000, matching the existingTOOL_CALL_STALL_THRESHOLD_MSso the two hang signals line up on dashboards.Expected vs unexpected
Rather than excluding legitimate waits, all four states are reported and tagged, with a derived
isExpectedboolean so queries can split the populations with a single predicate. Silence is only interpretable with the reason attached.hangReasonisExpectednoProgressfalsestalledAfterProgressfalsewaitingOnUsertruerunningTooltrueThis is documented on the
AgentHostTurnHangReasontype and in_deriveHangReason. A blocker outranks an in-flight tool call, since a tool awaiting confirmation is both and the human is the real reason for the silence.Client tool execution is deliberately not a user blocker. The protocol defines
SessionInputRequestKind.ToolClientExecutionas delegated running work rather than a prompt, and the session reducer'sawaitsUserpredicate excludes it for the same reason; counting it would report every long-running client tool as waiting on a human. It is represented by the in-flight tool set instead, yieldingrunningTool.Bounded volume
Each reason fires at most once per turn, and the watchdog re-arms for at most 6 quiet windows. Re-arming exists so a turn that moves from
waitingOnUserto genuinely stuck still gets reported; the cap keeps live timers and event volume bounded for a permanently dead turn.Recovery event
agentHost.hungTurnCompletedfires when a previously-hung turn later completes, carryinghangReportCount,totalTimeMsandtimeAfterHangMs— so permanent hangs can be separated from merely slow ones. Modelled on the existingstalledToolCallCompleted.Leak fix
_turnTimingspreviously kept entries for turns that never completed, forever. Turn tracking is now also cleared on channel teardown (clearChannelTelemetry, renamed fromclearToolCallTelemetrysince it now clears both trackers) and on truncation, closing that gap for the whole class of lost-turn bugs.Decision: kept
toolCallStalledalongside, not subsumedagentHost.toolCallStalledcarries tool identity (toolId,toolSourceKind) and only covers tool-specific blockers; the new event is turn-scoped. Subsuming it would mean either losing that tool detail or bloating the generic event with fields that are null for most hangs — and it would break existing queries. Keeping both also keeps this diff smaller and lower-risk.Reviewer notes
A review pass caught four issues; three are fixed here, one is deliberately left alone:
turnUnblockeddidn't restart the quiet clock — a user answering a confirmation at 4:59 would have been misreported as an unexplained stall one second later. Both blocking and unblocking now reset the quiet period and re-arm.ChatToolCallReadybypasses_dispatchActionForSession, so it never reached the watchdog. Now fed explicitly.clearTurnsExcept.waitingOnUser(review feedback) — now excluded from user blockers, matching the protocol'sawaitsUsersemantics.runningToolfor a tool that would never run. Cleared on both the client and agent dispatch paths.turnTracker.turnStarted, so the existingturnCompleted(subagent.chatUri, …)calls are already dead code today. Wiring that up would start emittingagentHost.turnCompletedfor subagent turns and visibly shift turn-count volume on existing dashboards — out of scope here. Subagent hangs are still caught viarunningToolon the parent turn.Activity is gated to turn-scoped actions (
hasKey(action, { turnId: true })). Session-scoped actions like MCP server state changes can arrive while a turn is genuinely stuck and would otherwise mask the hang.Testing
11 new tests in
agentHostTurnHangTelemetry.test.tsusingrunWithFakedTimers, driven throughAgentSideEffectsso the real wiring is exercised rather than the tracker in isolation:noProgressafter the threshold for a turn that starts and is never heard from again (the motivating case)stalledAfterProgressafter streaming stopswaitingOnUser, and a silent long-running toolrunningToolnpm run typecheck-clientclean, eslint clean, hygiene clean, and all 4404src/vs/platform/agentHosttests pass — including the two existing telemetry suites, which are unmodified.(Written by Copilot)