Environment
- Copilot CLI version: 1.0.36
- Extension SDK:
@github/copilot-sdk/extension (joinSession API)
- OS: Windows 11 (Windows_NT)
Summary
When a Copilot CLI extension calls session.rpc.ui.elicitation() from inside a command handler that previously called session.sendAndWait(), the elicitation resolves with { action: "decline" } in 1-2ms without ever presenting a modal to the user. The session reports capabilities.ui.elicitation = true and mode = interactive, yet the user never sees any prompt.
The extension's agent-mediated fallback (sending a prompt asking the model to call ask_user) also fails because the ask_user tool auto-declines with "The user is not available to respond" for turns injected by sendAndWait.
Net result: The extension cannot collect any human input after delegating work via sendAndWait, making interactive gates (review finding selection, publish attestation) impossible.
Steps to reproduce
- Load a Copilot CLI extension that registers a command via
joinSession({ commands: [...] })
- In the command handler, call
session.sendAndWait() to delegate to another command
- After
sendAndWait resolves, call session.rpc.ui.elicitation() with a valid schema
- Observe that it resolves instantly with
{ action: "decline" } --- no UI is shown
Minimal reproduction
import { joinSession } from "@github/copilot-sdk/extension";
const session = await joinSession({
commands: [
{
name: "test-elicit",
description: "Test elicitation after sendAndWait",
handler: async (context) => {
// Step 1: Delegate work to the model
await session.sendAndWait({ prompt: "Say hello" }, 60_000);
// Step 2: Try to elicit user input
const start = Date.now();
const result = await session.rpc.ui.elicitation({
message: "Pick an option",
requestedSchema: {
type: "object",
properties: {
choice: {
type: "string",
title: "Your choice",
enum: ["A", "B", "C"],
default: "A",
},
},
required: ["choice"],
},
});
const elapsed = Date.now() - start;
// Logs: "Elicitation returned decline in 1ms"
await session.log(
Elicitation returned {result.action} in {elapsed}ms
);
},
},
],
});
Observed behavior
Timeline from session events
T+0s session.info "interactive elicitation: available; session mode: unknown"
<- session.capabilities.ui.elicitation reports true
T+997s sendAndWait("/review ...") called, completes successfully
T+997s session.rpc.mode.set({ mode: "interactive" }) succeeds
<- mode is now "interactive"
T+997s session.rpc.ui.elicitation() called
<- returns { action: "decline" } in 1ms
<- NO MODAL IS SHOWN TO THE USER
T+997s Agent-mediated fallback: sendAndWait() with prompt asking model to call ask_user
<- model calls ask_user
<- ask_user returns: "The user is not available to respond"
<- model returns {"action":"decline"}
T+1024s Second elicitation attempt (text fallback)
<- session.rpc.ui.elicitation() returns decline in 2ms (again, no modal)
<- agent-mediated fallback also fails with "user not available"
Raw session event evidence
{
"type": "session.warning",
"data": {
"message": "Direct elicitation returned decline in 1ms (mode: interactive); treating as synthetic decline."
},
"timestamp": "2026-04-27T03:00:07.856Z"
}
{
"type": "tool.execution_complete",
"data": {
"toolTelemetry": {
"properties": {
"elicitation_action": "decline",
"elicitation_field_types": "[\"multi-enum\",\"string\"]"
}
},
"result": {
"content": "The user is not available to respond and will review your work later."
}
},
"timestamp": "2026-04-27T03:00:31.300Z"
}
Expected behavior
session.rpc.ui.elicitation() should present an interactive modal to the user and block until the user accepts, declines, or cancels. If the host cannot present a modal in the current execution context (e.g., inside a sendAndWait callback), it should either:
- Queue the elicitation and present it after the current turn completes, or
- Throw an error with a clear message like
"Elicitation not supported during sendAndWait callbacks", or
- Return a distinct result (e.g.,
{ action: "unavailable" }) so extensions can distinguish "host can't show UI" from "user clicked decline"
Additionally, ask_user should work for model turns injected by extensions via sendAndWait. The user IS present --- they typed a command and are waiting. The sendAndWait turn is extension-initiated, but the session is still interactive.
Call context where elicitation fails
User types: /submit-pr
-> handleSubmitPr()
-> runPreflight() <- works
-> sendAndWait("/review ...") <- works (30min timeout)
-> recordAndLogReviewOutput() <- works
-> askForReviewDecisions() <- FAILS HERE
-> elicit(message, schema)
-> session.rpc.ui.elicitation() <- returns decline in 1ms
-> agentMediatedElicitation()
-> sendAndWait(prompt) <- model calls ask_user
-> ask_user <- "user not available"
Extension workaround code (currently failing)
async function elicit(message, requestedSchema) {
if (!session.capabilities?.ui?.elicitation) {
return agentMediatedElicitation(message, requestedSchema, "unavailable");
}
const mode = await ensureInteractiveMode(message);
if (mode === "autopilot") {
return agentMediatedElicitation(message, requestedSchema, "autopilot");
}
const elicitStart = Date.now();
const result = await session.rpc.ui.elicitation({ message, requestedSchema });
const elicitMs = Date.now() - elicitStart;
// Detect synthetic decline (returned too fast for human interaction)
const isSyntheticDecline =
result.action === "decline" &&
(["autopilot", "unknown"].includes(await refreshSessionMode())
|| elicitMs < 2000);
if (isSyntheticDecline) {
return agentMediatedElicitation(message, requestedSchema,
�licitation-returned-in-{elicitMs}ms);
}
return result;
}
async function agentMediatedElicitation(message, requestedSchema, reason) {
// Asks the model to call ask_user on behalf of the extension
// FAILS: ask_user auto-declines for sendAndWait-injected turns
const response = await sendAndCaptureContent(
agentElicitationPrompt(message, requestedSchema, reason),
15 * 60 * 1000
);
// ...parse JSON response...
}
Both layers fail:
- Layer 1 fails because
session.rpc.ui.elicitation() returns decline in 1ms
- Layer 2 fails because
ask_user auto-declines for turns injected by sendAndWait
Impact
- Extensions cannot implement interactive multi-step workflows
session.capabilities.ui.elicitation capability check is misleading --- reports true but doesn't work in practice
- Extensions requiring human-in-the-loop gates (security attestation, publish confirmation) cannot enforce them
- The
sendAndWait -> elicit pattern is the most natural way to build multi-step extension workflows, and it's completely broken
Environment
@github/copilot-sdk/extension(joinSessionAPI)Summary
When a Copilot CLI extension calls
session.rpc.ui.elicitation()from inside a command handler that previously calledsession.sendAndWait(), the elicitation resolves with{ action: "decline" }in 1-2ms without ever presenting a modal to the user. The session reportscapabilities.ui.elicitation = trueandmode = interactive, yet the user never sees any prompt.The extension's agent-mediated fallback (sending a prompt asking the model to call
ask_user) also fails because theask_usertool auto-declines with "The user is not available to respond" for turns injected bysendAndWait.Net result: The extension cannot collect any human input after delegating work via
sendAndWait, making interactive gates (review finding selection, publish attestation) impossible.Steps to reproduce
joinSession({ commands: [...] })session.sendAndWait()to delegate to another commandsendAndWaitresolves, callsession.rpc.ui.elicitation()with a valid schema{ action: "decline" }--- no UI is shownMinimal reproduction
Observed behavior
Timeline from session events
Raw session event evidence
{ "type": "session.warning", "data": { "message": "Direct elicitation returned decline in 1ms (mode: interactive); treating as synthetic decline." }, "timestamp": "2026-04-27T03:00:07.856Z" } { "type": "tool.execution_complete", "data": { "toolTelemetry": { "properties": { "elicitation_action": "decline", "elicitation_field_types": "[\"multi-enum\",\"string\"]" } }, "result": { "content": "The user is not available to respond and will review your work later." } }, "timestamp": "2026-04-27T03:00:31.300Z" }Expected behavior
session.rpc.ui.elicitation()should present an interactive modal to the user and block until the user accepts, declines, or cancels. If the host cannot present a modal in the current execution context (e.g., inside asendAndWaitcallback), it should either:"Elicitation not supported during sendAndWait callbacks", or{ action: "unavailable" }) so extensions can distinguish "host can't show UI" from "user clicked decline"Additionally,
ask_usershould work for model turns injected by extensions viasendAndWait. The user IS present --- they typed a command and are waiting. ThesendAndWaitturn is extension-initiated, but the session is still interactive.Call context where elicitation fails
Extension workaround code (currently failing)
Both layers fail:
session.rpc.ui.elicitation()returnsdeclinein 1msask_userauto-declines for turns injected bysendAndWaitImpact
session.capabilities.ui.elicitationcapability check is misleading --- reportstruebut doesn't work in practicesendAndWait->elicitpattern is the most natural way to build multi-step extension workflows, and it's completely broken