Skip to content

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

@Ofekw

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions