session.rpc.ui.elicitation() returns instant decline (1-2ms) without showing modal after sendAndWait

Open
#1,149 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
45/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Quiet
Tech stack
javascript, node.js

Research direction

Start at the joinSession command handler and trace the session.sendAndWait() and session.rpc.ui.elicitation() paths, including the ask_user fallback. Reproduce the instant decline on Windows with the minimal example and inspect the session events. Done means interactive elicitation can collect input after sendAndWait, or the extension receives a distinct, actionable unavailable result.

Written by the indexing model from the issue text.

Description

documentation

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

  1. Load a Copilot CLI extension that registers a command via joinSession({ commands: [...] })
  2. In the command handler, call session.sendAndWait() to delegate to another command
  3. After sendAndWait resolves, call session.rpc.ui.elicitation() with a valid schema
  4. 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:

  1. Queue the elicitation and present it after the current turn completes, or
  2. Throw an error with a clear message like "Elicitation not supported during sendAndWait callbacks", or
  3. 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
Dominant language
Java
Stars
10.5k
Forks
1.5k
Avg merge
1d 12h
Merged PRs (30d)
133

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from github/copilot-sdk

All issues in github/copilot-sdk

Similar issues

More Java issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.