github / github/copilot-sdk

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

Abierto
#1,149 2 comentarios 0 reacciones 0 asignados Ver en GitHub
documentation
Lenguaje dominante
Java
Estrellas
10.5k
Forks
1.5k
Merge medio
1 d 11 h
PR fusionados (30 d)
128

Descripción

## 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

```js
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

```json
{
"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)

```js
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

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.