feat: support asynchronous user questions through AIR - #484
feat: support asynchronous user questions through AIR#484nikita-ashihmin wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The reply payload envelope is built from user-provided answers and should defensively avoid tag-delimiter injection/ambiguity (see stored security comment).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds first-class support for Codex “asynchronous user questions” by negotiating an AIR asyncQuestions capability, sending a dedicated client RPC (_session/async_question/request) without blocking the notification stream, and delivering the user’s answers back to Codex via the existing steering queue (joining an active turn or starting a new one).
Changes:
- Introduces
CodexAsyncQuestionHandler+ AIR capability wiring to open async-question RPCs and feed answers back as steering input, with cancellation/close/provider-restart handling. - Updates steering pipeline to propagate
AbortSignalthroughSteeringQueueand steering execution so queued async answers can be cancelled cleanly. - Adds tests + snapshots and documents the client contract and examples in
docs/async-questions.md.
File summaries
| File | Description |
|---|---|
| src/SteeringQueue.ts | Adds optional AbortSignal propagation through queued steering requests. |
| src/CodexEventHandler.ts | Ensures async-question agent messages are emitted even if they arrive only as completed items (no deltas), while deduplicating against deltas. |
| src/CodexAsyncQuestionHandler.ts | New handler that opens _session/async_question/request, validates responses, and delivers answers via steering with cancellation support. |
| src/CodexAcpServer.ts | Wires handler into capability negotiation, prompt lifecycle, cancellation/close, and provider/process shutdown paths; tightens steering retry semantics. |
| src/AsyncQuestionExtension.ts | Defines the AIR asyncQuestions request/response types and method name constant. |
| src/AirExtension.ts | Adds AIR_ASYNC_QUESTIONS_KEY capability constant. |
| src/AcpExtensions.ts | Re-exports async-question extension types for consumers. |
| src/tests/CodexACPAgent/async-questions.test.ts | Comprehensive event-driven coverage for negotiation, dedupe, late answers, cancellation, and transport failure behaviors. |
| src/tests/CodexACPAgent/initialize.test.ts | Updates advertised AIR capability list to include asyncQuestions. |
| src/tests/CodexACPAgent/snapshots/* | Adds snapshots for request/updates and late-answer input payload. |
| src/tests/acp-test-utils.ts | Adds fixture support for overriding extension RPC responses. |
| readme-dev.md | Links dev docs to the new async-question contract. |
| docs/async-questions.md | Documents the client contract, lifecycle, cancellation semantics, and examples. |
Review details
- Files reviewed: 14/14 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const result = await this.deliver({ | ||
| sessionId: request.sessionId, | ||
| prompt: [{type: "text", text: `<send_user_message_question_reply>\n${JSON.stringify(replies)}\n</send_user_message_question_reply>`}], | ||
| }, signal); |
There was a problem hiding this comment.
🔵 Needs a closer look
The concurrency behavior is well tested, but the live Codex question exchange remains unverified because smoke testing was blocked.
Review details
- Files reviewed: 15/15 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Async questions emitted by native subagents are filtered out before the AIR request handler.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 17/17 changed files
- Comments generated: 1
- Review effort level: Balanced
| if (!activePrompt.signal.aborted && !this.sessionIsClosing(params.sessionId) | ||
| && "threadId" in event.params && event.params.threadId === params.sessionId) { | ||
| this.asyncQuestions.handleNotification(event, this.clientCapabilities); |
Codex asynchronous questions now reach clients through the AIR
asyncQuestionscapability and_session/async_question/request. The question RPC waits for the user while the agent continues working. Answers become user input through the existing steering queue, joining an active turn or starting a new one. Clients without the capability receive ordinary question text.Adds the client contract and examples in
docs/async-questions.md. Pending questions survive normal turn completion, but cancellation and session shutdown invalidate them. History does not reopen forms. Steering retries only an explicit “no active turn” rejection, avoiding duplicate input after an uncertain transport failure.Validation:
npm run typechecknpm test: 575 passed, 26 skippednpm run bundle:allnpm run codex-test:async-questions: passed with locked Codex 0.153.3 andgpt-6-astra; real async question, AIR RPC, late answer, new-turn input, and model confirmation.Live validation is reproducible after
npm ciwith an authenticated Codex account. The smoke test asserts the actual question RPC and reply payload; a successful prompt alone does not count as a pass. AIR clients must implement the documented form before advertising the capability.