-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Fix MCP elicitation deadlock and improve UX #6650
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Fix deadlock in agent.rs where elicitation messages weren't being delivered to the UI. The tool execution loop now uses tokio::select! with a 100ms timeout to periodically check for elicitation messages even when tools are blocked waiting for user input. - Add countdown timer to ElicitationRequest component so users know they have 5 minutes to respond. Timer turns red when under 60 seconds and shows an expired state when time runs out. - Add 'Waiting for your response' indicator with pulsing clock icon to make it clear the system is waiting for user input.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Fixes a deadlock where MCP elicitation requests could time out immediately by ensuring the agent loop continues to drain elicitation messages, and improves the desktop UI by showing a visible countdown/expiry state for elicitation prompts.
Changes:
- Update the agent tool execution loop to periodically poll for elicitation messages via
tokio::select!+ timeout. - Add an elicitation countdown timer, urgent styling, and an “expired” UI state in the desktop app.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| ui/desktop/src/components/ElicitationRequest.tsx | Adds countdown timer + urgent/expired UI for elicitation requests. |
| crates/goose/src/agents/agent.rs | Prevents elicitation deadlock by draining elicitation messages while waiting on tool streams. |
| const [submitted, setSubmitted] = useState(isClicked); | ||
| const [timeRemaining, setTimeRemaining] = useState(ELICITATION_TIMEOUT_SECONDS); | ||
| const startTimeRef = useRef(Date.now()); | ||
|
|
||
| useEffect(() => { | ||
| if (submitted || isCancelledMessage || isClicked) return; |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The countdown is anchored to startTimeRef = Date.now() on mount and never re-initialized for a new elicitationId (or persisted across unmount/remount), so the UI can show an incorrect remaining time after navigation or if the component is reused. Consider keying the start timestamp by elicitationId (e.g., a module-level Map) and/or resetting startTimeRef.current + timeRemaining whenever elicitationId changes (and include elicitationId in the effect deps).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The timer resets on mount, which is intentional. If the user navigates away, the server-side timeout continues and the elicitation will expire. When they return, it will be a new elicitation request with a new ID, so a fresh timer is correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| const startTimeRef = useRef(Date.now()); | ||
|
|
||
| useEffect(() => { | ||
| if (submitted || isCancelledMessage || isClicked) return; | ||
|
|
||
| const interval = setInterval(() => { | ||
| const elapsed = Math.floor((Date.now() - startTimeRef.current) / 1000); | ||
| const remaining = Math.max(0, ELICITATION_TIMEOUT_SECONDS - elapsed); | ||
| setTimeRemaining(remaining); | ||
|
|
||
| if (remaining === 0) { | ||
| clearInterval(interval); | ||
| } | ||
| }, 1000); | ||
|
|
||
| return () => clearInterval(interval); | ||
| }, [submitted, isCancelledMessage, isClicked]); | ||
|
|
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The countdown is anchored to component mount time (useRef(Date.now())), so if the elicitation message is rendered from history (or otherwise mounts late) the UI can incorrectly show a fresh 5:00 remaining even though the server-side 5 minute timeout has already partially/fully elapsed; consider basing the start time on the message’s created timestamp (pass it in) and/or resetting startTimeRef/timeRemaining when actionRequiredContent.data.id changes.
|
/goose please review |
|
Summary: This PR fixes a real deadlock in MCP elicitation where the tool loop blocked waiting for results while elicitation messages couldn't be drained. The polling approach with 🟡 Warnings
🟢 Suggestions
✅ Highlights
Review generated by goose |
* origin/main: fix: dispatch ADD_ACTIVE_SESSION event before navigating from "View All" (#6679) Speed up Databricks provider init by removing fetch of supported models (#6616) fix: correct typos in documentation and Justfile (#6686) docs: frameDomains and baseUriDomains for mcp apps (#6684) docs: add Remotion video creation tutorial (#6675) docs: export recipe and copy yaml (#6680) Test against fastmcp (#6666) docs: mid-session changes (#6672) Fix MCP elicitation deadlock and improve UX (#6650) chore: upgrade to rmcp 0.14.0 (#6674) [docs] add MCP-UI to MCP Apps blog (#6664) ACP get working dir from args.cwd (#6653) Optimise load config in UI (#6662) # Conflicts: # ui/desktop/src/components/Layout/AppLayout.tsx
…o dkatz/canonical-context * 'dkatz/canonical-provider' of github.com:block/goose: (27 commits) docs: add Remotion video creation tutorial (#6675) docs: export recipe and copy yaml (#6680) Test against fastmcp (#6666) docs: mid-session changes (#6672) Fix MCP elicitation deadlock and improve UX (#6650) chore: upgrade to rmcp 0.14.0 (#6674) [docs] add MCP-UI to MCP Apps blog (#6664) ACP get working dir from args.cwd (#6653) Optimise load config in UI (#6662) Fix GCP Vertex AI global endpoint support for Gemini 3 models (#6187) fix: macOS keychain infinite prompt loop (#6620) chore: reduce duplicate or unused cargo deps (#6630) feat: codex subscription support (#6600) smoke test allow pass for flaky providers (#6638) feat: Add built-in skill for goose documentation reference (#6534) Native images (#6619) docs: ml-based prompt injection detection (#6627) Strip the audience for compacting (#6646) chore(release): release version 1.21.0 (minor) (#6634) add collapsable chat nav (#6649) ...
Summary
Fixes a deadlock in MCP elicitation that caused requests to timeout immediately, and improves the user experience by adding a visible countdown timer.
Before it would say my request immediately timed out

After, it would actually give some time for the time out and give the user an indication that it will time out
Screen.Recording.2026-01-22.at.4.21.26.PM.mov
Problem
When an MCP server called
elicitInput(), the elicitation form would appear in the UI but immediately timeout. This was caused by a deadlock:elicitInput()→ blocks waiting for user responsecombined.next().awaitfor tool resultsdrain_elicitation_messages()only runs after a tool yieldsSolution
Rust (agent.rs)
Changed the tool execution loop to use
tokio::select!with a 100ms timeout. This allows the loop to periodically check for elicitation messages even when tools are blocked:UI (ElicitationRequest.tsx)
Added visual feedback so users understand the time constraint:
Testing
elicitInput()