Skip to content

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

Description

@Ofekw

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions