session.rpc.ui.elicitation() returns instant decline (1-2ms) without showing modal after sendAndWait
- 主要言語
- Java
- スター
- 10.5k
- フォーク
- 1.5k
- 平均マージ
- 1日 11時間
- マージ済み PR(30日)
- 127
説明
## 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
コントリビューションガイド
評価
この issue はまだ評価されていません。