@agentclientprotocol/codex-acp 2.5.0 → 2.5.1-preview.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +365 -125
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -17,6 +17,7 @@ Use [OpenAI Codex](https://github.com/openai/codex) from [Agent Client Protocol]
17
17
  - For AIR, one tool call shape with each fact in one field, as the [tool call contract](docs/air-extensions.md#tool-call-contract) defines. Other clients keep the earlier fields.
18
18
  - [Native ACP subagent sessions](docs/subagent-sessions.md) (after capability negotiation) with separate child histories and root-routed permissions; a legacy tool-call fallback otherwise.
19
19
  - Mid-turn input through the [_session/steering extension](docs/steering.md), with an opt-in host-owned fallback when no turn can accept it.
20
+ - [Async Codex questions](docs/async-questions.md) as non-blocking form elicitations, with answers steered into the turn that asked them.
20
21
  - [Background terminal tasks](docs/air-extensions.md#async-tasks) in AIR, with task status and targeted stop support after capability negotiation.
21
22
  - Session-scoped long-running goals in AIR through the [goal extension](docs/air-extensions.md#goal).
22
23
  - Typed warnings and errors through the opt-in [AIR session failure extension](docs/air-extensions.md#session-failure).
package/dist/index.js CHANGED
@@ -32366,8 +32366,19 @@ var AcpV2Connection = class {
32366
32366
  return toV1RequestPermissionResponse(response);
32367
32367
  }, options?.cancellationSignal);
32368
32368
  }
32369
- /** Request-scoped login elicitations and idle background elicitations never change state. */
32370
- async createElicitation(request, options) {
32369
+ /**
32370
+ * Sends `elicitation/create`. Request-scoped login elicitations and idle background elicitations never change
32371
+ * the session state.
32372
+ *
32373
+ * @param request The elicitation to show.
32374
+ * @param options Request options, such as the signal that cancels the request.
32375
+ * @param interaction.userAction Whether the agent waits for the answer, `true` by default. While such an
32376
+ * elicitation is open in a running turn, the session reports `requires_action` whenever none of its items
32377
+ * is in progress. Pass `false` for a question the agent keeps working through, so the session state is left
32378
+ * as it is.
32379
+ */
32380
+ async createElicitation(request, options, interaction = {}) {
32381
+ const userAction = interaction.userAction ?? true;
32371
32382
  const sendRequest = async () => {
32372
32383
  const response = await this.client.request(
32373
32384
  methods2.client.elicitation.create,
@@ -32377,7 +32388,7 @@ var AcpV2Connection = class {
32377
32388
  return toV1CreateElicitationResponse(response);
32378
32389
  };
32379
32390
  const sessionId = elicitationSessionId(request);
32380
- if (sessionId === void 0) {
32391
+ if (sessionId === void 0 || !userAction) {
32381
32392
  return sendRequest();
32382
32393
  }
32383
32394
  const toolCallId = "toolCallId" in request && typeof request.toolCallId === "string" ? request.toolCallId : void 0;
@@ -37613,12 +37624,16 @@ ${params.turnId}`;
37613
37624
  }
37614
37625
  return this.createAgentMessageText(citations.push(event.delta), event.itemId, event.threadId);
37615
37626
  }
37616
- /** Sends the text a file citation held back, once the message is complete. */
37617
- flushAgentMessageText(itemId, threadId) {
37627
+ /**
37628
+ * Sends the text a file citation held back, once the message is complete. A message that
37629
+ * arrives whole, without deltas (an async question, for one), is sent in full.
37630
+ */
37631
+ flushAgentMessageText(item, threadId) {
37618
37632
  const streams = this.streamsFor(threadId);
37619
- const rest = streams.agentMessageCitations.get(itemId)?.flush() ?? "";
37620
- streams.agentMessageCitations.delete(itemId);
37621
- return this.createAgentMessageText(rest, itemId, threadId);
37633
+ const citations = streams.agentMessageCitations.get(item.id);
37634
+ streams.agentMessageCitations.delete(item.id);
37635
+ const text = citations ? citations.flush() : renderFileCitations(item.text, this.sessionState.cwd);
37636
+ return this.createAgentMessageText(text, item.id, threadId);
37622
37637
  }
37623
37638
  createAgentMessageText(text, itemId, threadId) {
37624
37639
  if (text.length === 0) return null;
@@ -37773,7 +37788,7 @@ ${event.details}` : event.summary;
37773
37788
  return this.renderer.render(this.subagents.legacyCollaborationCompleted(event.item));
37774
37789
  case "agentMessage":
37775
37790
  this.rememberAgentMessagePhase(event.item, event.threadId);
37776
- return this.flushAgentMessageText(event.item.id, event.threadId);
37791
+ return this.flushAgentMessageText(event.item, event.threadId);
37777
37792
  case "plan":
37778
37793
  return await this.createCompletedPlanEvent(event.item);
37779
37794
  case "exitedReviewMode":
@@ -38953,6 +38968,119 @@ function userInputResponseValue(content, fieldId) {
38953
38968
  }
38954
38969
  return value;
38955
38970
  }
38971
+ function buildUserInputSchema(questions, clientCapabilities, answersRequired) {
38972
+ const properties = {};
38973
+ const required2 = [];
38974
+ const questionIds = new Set(questions.map((question) => question.id));
38975
+ const airClient = isAirClient(clientCapabilities);
38976
+ for (const question of questions) {
38977
+ const options = question.options ?? [];
38978
+ const hasOptions = options.length > 0;
38979
+ const hasOtherAnswer = question.isOther && hasOptions;
38980
+ const base = {
38981
+ title: question.question || question.header || question.id,
38982
+ ...question.header ? { description: question.header } : {},
38983
+ _meta: {
38984
+ codex: {
38985
+ isOther: question.isOther,
38986
+ isSecret: question.isSecret
38987
+ }
38988
+ }
38989
+ };
38990
+ if (answersRequired) {
38991
+ required2.push(question.id);
38992
+ }
38993
+ properties[question.id] = hasOptions ? {
38994
+ ...base,
38995
+ type: "string",
38996
+ oneOf: [
38997
+ ...options.map((option) => ({
38998
+ const: option.label,
38999
+ title: option.label,
39000
+ ...option.description ? { description: option.description } : {}
39001
+ })),
39002
+ ...hasOtherAnswer && !airClient && !options.some((option) => option.label === USER_INPUT_OTHER_OPTION) ? [{
39003
+ const: USER_INPUT_OTHER_OPTION,
39004
+ title: USER_INPUT_OTHER_OPTION,
39005
+ description: "Provide a different answer in the note field."
39006
+ }] : []
39007
+ ]
39008
+ } : {
39009
+ ...base,
39010
+ type: "string"
39011
+ };
39012
+ if (hasOtherAnswer) {
39013
+ const noteMeta = {
39014
+ codex: {
39015
+ questionId: question.id,
39016
+ role: "user_note",
39017
+ isSecret: question.isSecret
39018
+ }
39019
+ };
39020
+ properties[userInputNoteFieldId(question.id, questionIds)] = {
39021
+ type: "string",
39022
+ title: "Additional answer or note",
39023
+ _meta: airClient ? withAirMeta({ ...noteMeta, [LEGACY_AIR_CUSTOM_ANSWER_KEY]: true }, AIR_CUSTOM_ANSWER_KEY, true) : noteMeta
39024
+ };
39025
+ }
39026
+ }
39027
+ return {
39028
+ type: "object",
39029
+ properties,
39030
+ ...answersRequired ? { required: required2 } : {}
39031
+ };
39032
+ }
39033
+ function buildUserInputRequest(params, clientCapabilities) {
39034
+ return {
39035
+ sessionId: params.threadId,
39036
+ toolCallId: params.itemId,
39037
+ mode: "form",
39038
+ message: "Codex needs your input to continue.",
39039
+ requestedSchema: buildUserInputSchema(params.questions, clientCapabilities, true),
39040
+ _meta: {
39041
+ codex: {
39042
+ autoResolutionMs: params.autoResolutionMs
39043
+ }
39044
+ }
39045
+ };
39046
+ }
39047
+ function userInputAnswers(response, questions, clientCapabilities) {
39048
+ if (!CreateElicitationResponse.isAccept(response)) {
39049
+ return {};
39050
+ }
39051
+ const answers = {};
39052
+ const content = contentRecord(response.content);
39053
+ const questionIds = new Set(questions.map((question) => question.id));
39054
+ const airClient = isAirClient(clientCapabilities);
39055
+ for (const question of questions) {
39056
+ const answerValues = [];
39057
+ const hasOtherAnswer = question.isOther && question.options != null && question.options.length > 0;
39058
+ const value = userInputResponseValue(content, question.id);
39059
+ const typedAnswer = airClient && hasOtherAnswer ? typedChoiceAnswer(value, question) : void 0;
39060
+ if (typedAnswer !== void 0) {
39061
+ answerValues.push(USER_INPUT_OTHER_OPTION, `${USER_INPUT_NOTE_PREFIX}${typedAnswer.trim()}`);
39062
+ } else if (value !== void 0) {
39063
+ answerValues.push(...Array.isArray(value) ? value.map(String) : [String(value)]);
39064
+ }
39065
+ if (hasOtherAnswer) {
39066
+ const note = userInputResponseValue(content, userInputNoteFieldId(question.id, questionIds));
39067
+ if (note !== void 0) {
39068
+ const notes = Array.isArray(note) ? note : [note];
39069
+ answerValues.push(...notes.map((item) => `${USER_INPUT_NOTE_PREFIX}${String(item).trim()}`));
39070
+ }
39071
+ }
39072
+ if (answerValues.length === 0) {
39073
+ continue;
39074
+ }
39075
+ answers[question.id] = {
39076
+ answers: answerValues
39077
+ };
39078
+ }
39079
+ return answers;
39080
+ }
39081
+ function userInputAnswerText(answers) {
39082
+ return answers.filter((answer) => answer !== USER_INPUT_OTHER_OPTION).map((answer) => answer.startsWith(USER_INPUT_NOTE_PREFIX) ? answer.slice(USER_INPUT_NOTE_PREFIX.length) : answer).filter((answer) => answer.trim().length > 0).join("\n\n");
39083
+ }
38956
39084
  var CodexElicitationHandler = class {
38957
39085
  connection;
38958
39086
  renderer;
@@ -39061,7 +39189,7 @@ var CodexElicitationHandler = class {
39061
39189
  if (response === null) {
39062
39190
  return { answers: {} };
39063
39191
  }
39064
- return this.convertUserInputResponse(response, params);
39192
+ return { answers: userInputAnswers(response, params.questions, this.clientCapabilities) };
39065
39193
  } catch (error62) {
39066
39194
  logger.error("Error handling Codex user input request", error62);
39067
39195
  return { answers: {} };
@@ -39071,7 +39199,7 @@ var CodexElicitationHandler = class {
39071
39199
  return cancellationSignal ? { cancellationSignal } : void 0;
39072
39200
  }
39073
39201
  async requestUserInputElicitation(params) {
39074
- const request = this.buildUserInputRequest(params);
39202
+ const request = buildUserInputRequest(params, this.clientCapabilities);
39075
39203
  const signal = subagentInteractionSignal(params.threadId, this.cancellationSignal);
39076
39204
  if (params.autoResolutionMs === null) {
39077
39205
  return await withSubagentUserAction(params.threadId, () => this.connection.request(
@@ -39171,77 +39299,6 @@ var CodexElicitationHandler = class {
39171
39299
  };
39172
39300
  }
39173
39301
  }
39174
- buildUserInputRequest(params) {
39175
- const properties = {};
39176
- const required2 = [];
39177
- const questionIds = new Set(params.questions.map((question) => question.id));
39178
- const airClient = isAirClient(this.clientCapabilities);
39179
- for (const question of params.questions) {
39180
- const options = question.options ?? [];
39181
- const hasOptions = options.length > 0;
39182
- const hasOtherAnswer = question.isOther && hasOptions;
39183
- const base = {
39184
- title: question.question || question.header || question.id,
39185
- ...question.header ? { description: question.header } : {},
39186
- _meta: {
39187
- codex: {
39188
- isOther: question.isOther,
39189
- isSecret: question.isSecret
39190
- }
39191
- }
39192
- };
39193
- required2.push(question.id);
39194
- properties[question.id] = hasOptions ? {
39195
- ...base,
39196
- type: "string",
39197
- oneOf: [
39198
- ...options.map((option) => ({
39199
- const: option.label,
39200
- title: option.label,
39201
- ...option.description ? { description: option.description } : {}
39202
- })),
39203
- ...hasOtherAnswer && !airClient && !options.some((option) => option.label === USER_INPUT_OTHER_OPTION) ? [{
39204
- const: USER_INPUT_OTHER_OPTION,
39205
- title: USER_INPUT_OTHER_OPTION,
39206
- description: "Provide a different answer in the note field."
39207
- }] : []
39208
- ]
39209
- } : {
39210
- ...base,
39211
- type: "string"
39212
- };
39213
- if (hasOtherAnswer) {
39214
- const noteMeta = {
39215
- codex: {
39216
- questionId: question.id,
39217
- role: "user_note",
39218
- isSecret: question.isSecret
39219
- }
39220
- };
39221
- properties[userInputNoteFieldId(question.id, questionIds)] = {
39222
- type: "string",
39223
- title: "Additional answer or note",
39224
- _meta: airClient ? withAirMeta({ ...noteMeta, [LEGACY_AIR_CUSTOM_ANSWER_KEY]: true }, AIR_CUSTOM_ANSWER_KEY, true) : noteMeta
39225
- };
39226
- }
39227
- }
39228
- return {
39229
- sessionId: params.threadId,
39230
- toolCallId: params.itemId,
39231
- mode: "form",
39232
- message: "Codex needs your input to continue.",
39233
- requestedSchema: {
39234
- type: "object",
39235
- properties,
39236
- required: required2
39237
- },
39238
- _meta: {
39239
- codex: {
39240
- autoResolutionMs: params.autoResolutionMs
39241
- }
39242
- }
39243
- };
39244
- }
39245
39302
  convertElicitationResponse(response, context) {
39246
39303
  if (CreateElicitationResponse.isAccept(response)) {
39247
39304
  const content = contentRecord(response.content);
@@ -39272,40 +39329,6 @@ var CodexElicitationHandler = class {
39272
39329
  isAllowedToolApprovalPersist(persist, persistOptions) {
39273
39330
  return persist === void 0 || persist === "once" || persist === "session" && persistOptions.has("session") || persist === "always" && persistOptions.has("always");
39274
39331
  }
39275
- convertUserInputResponse(response, params) {
39276
- if (!CreateElicitationResponse.isAccept(response)) {
39277
- return { answers: {} };
39278
- }
39279
- const answers = {};
39280
- const content = contentRecord(response.content);
39281
- const questionIds = new Set(params.questions.map((question) => question.id));
39282
- const airClient = isAirClient(this.clientCapabilities);
39283
- for (const question of params.questions) {
39284
- const answerValues = [];
39285
- const hasOtherAnswer = question.isOther && question.options != null && question.options.length > 0;
39286
- const value = userInputResponseValue(content, question.id);
39287
- const typedAnswer = airClient && hasOtherAnswer ? typedChoiceAnswer(value, question) : void 0;
39288
- if (typedAnswer !== void 0) {
39289
- answerValues.push(USER_INPUT_OTHER_OPTION, `${USER_INPUT_NOTE_PREFIX}${typedAnswer.trim()}`);
39290
- } else if (value !== void 0) {
39291
- answerValues.push(...Array.isArray(value) ? value.map(String) : [String(value)]);
39292
- }
39293
- if (hasOtherAnswer) {
39294
- const note = userInputResponseValue(content, userInputNoteFieldId(question.id, questionIds));
39295
- if (note !== void 0) {
39296
- const notes = Array.isArray(note) ? note : [note];
39297
- answerValues.push(...notes.map((item) => `${USER_INPUT_NOTE_PREFIX}${String(item).trim()}`));
39298
- }
39299
- }
39300
- if (answerValues.length === 0) {
39301
- continue;
39302
- }
39303
- answers[question.id] = {
39304
- answers: answerValues
39305
- };
39306
- }
39307
- return { answers };
39308
- }
39309
39332
  async publishAcceptedMcpToolApproval(sessionId, context, accepted) {
39310
39333
  if (!accepted || context.correlatedCallId === void 0) {
39311
39334
  return;
@@ -40552,7 +40575,7 @@ var package_default = {
40552
40575
  publishConfig: {
40553
40576
  access: "public"
40554
40577
  },
40555
- version: "2.5.0",
40578
+ version: "2.5.1-preview.2",
40556
40579
  description: "",
40557
40580
  main: "dist/index.js",
40558
40581
  bin: {
@@ -46567,6 +46590,198 @@ function isJetBrains2026_1Client(clientInfo) {
46567
46590
  return (isIntelliJPlatform || isJetBrainsClient) && clientInfo.version.startsWith("2026.1");
46568
46591
  }
46569
46592
 
46593
+ // src/CodexAsyncQuestions.ts
46594
+ import { randomUUID as randomUUID4 } from "node:crypto";
46595
+ var TOOL_NAME = "request_user_input_async";
46596
+ var REPLY_START = "<send_user_message_question_reply>";
46597
+ var REPLY_END = "</send_user_message_question_reply>";
46598
+ var MAX_REPLY_FIELD_LENGTH = 512;
46599
+ function asyncQuestionItemId(messageId, index) {
46600
+ return JSON.stringify([TOOL_NAME, messageId, index]);
46601
+ }
46602
+ function formatAsyncQuestionReply(replies) {
46603
+ const bounded = replies.map((reply) => ({
46604
+ ...reply,
46605
+ question: [...reply.question].slice(0, MAX_REPLY_FIELD_LENGTH).join("").replace(/[\r\n]/g, " ")
46606
+ }));
46607
+ if (bounded.some((reply) => reply.questionItemId.length > MAX_REPLY_FIELD_LENGTH)) {
46608
+ return displayReplies(bounded);
46609
+ }
46610
+ const body = bounded.map(({ answer, question, questionItemId }) => ({ answer, question, questionItemId }));
46611
+ return `${REPLY_START}
46612
+ ${JSON.stringify(body)}
46613
+ ${REPLY_END}`;
46614
+ }
46615
+ function parseAsyncQuestionReply(text) {
46616
+ const trimmed = text.trim();
46617
+ if (!trimmed.startsWith(REPLY_START) || !trimmed.endsWith(REPLY_END)) return null;
46618
+ let body;
46619
+ try {
46620
+ body = JSON.parse(trimmed.slice(REPLY_START.length, trimmed.length - REPLY_END.length));
46621
+ } catch {
46622
+ return null;
46623
+ }
46624
+ const replies = Array.isArray(body) ? body : [body];
46625
+ if (replies.length === 0 || !replies.every(isReply)) return null;
46626
+ return replies.map(({ questionItemId, question, answer }) => ({ questionItemId, question, answer }));
46627
+ }
46628
+ function renderAsyncQuestionReply(text) {
46629
+ const replies = parseAsyncQuestionReply(text);
46630
+ return replies === null ? null : displayReplies(replies);
46631
+ }
46632
+ function displayReplies(replies) {
46633
+ return replies.map((reply) => `> ${reply.question}
46634
+
46635
+ ${reply.answer}`).join("\n\n");
46636
+ }
46637
+ function isReply(value) {
46638
+ if (typeof value !== "object" || value === null) return false;
46639
+ const reply = value;
46640
+ return typeof reply["questionItemId"] === "string" && typeof reply["question"] === "string" && typeof reply["answer"] === "string";
46641
+ }
46642
+ var AsyncQuestionClient = class {
46643
+ /**
46644
+ * @param sessionId The session that the questions belong to.
46645
+ * @param connection The connection to the ACP client.
46646
+ * @param clientCapabilities The capabilities that the ACP client declared.
46647
+ * @param codexAcpClient Returns the current Codex client, which changes when the app-server restarts.
46648
+ */
46649
+ constructor(sessionId, connection, clientCapabilities, codexAcpClient) {
46650
+ this.sessionId = sessionId;
46651
+ this.connection = connection;
46652
+ this.clientCapabilities = clientCapabilities;
46653
+ this.codexAcpClient = codexAcpClient;
46654
+ }
46655
+ sessionId;
46656
+ connection;
46657
+ clientCapabilities;
46658
+ codexAcpClient;
46659
+ /** Sends the elicitation without marking the session as waiting for the user. */
46660
+ createElicitation(request, signal) {
46661
+ const v2 = AcpV2Connection.of(this.connection);
46662
+ return v2 !== null ? v2.createElicitation(request, { cancellationSignal: signal }, { userAction: false }) : this.connection.request(methods.client.elicitation.create, request, { cancellationSignal: signal });
46663
+ }
46664
+ /** Steers the text into the turn; rejects if that turn is no longer running. */
46665
+ async steer(turnId, text) {
46666
+ await this.codexAcpClient().steerTurn({
46667
+ threadId: this.sessionId,
46668
+ turnId,
46669
+ prompt: [{ type: "text", text }],
46670
+ clientUserMessageId: randomUUID4()
46671
+ });
46672
+ }
46673
+ /** Tells the user that an answer was not sent, if the client supports notices. */
46674
+ async reportDroppedAnswer() {
46675
+ if (!clientSupportsNotices(this.clientCapabilities)) return;
46676
+ await this.connection.notify(methods.client.session.update, {
46677
+ sessionId: this.sessionId,
46678
+ update: createSessionNotice(
46679
+ "warning",
46680
+ "Answer not sent",
46681
+ "Codex finished before your answer arrived. Send it as a new message if it still matters."
46682
+ )
46683
+ });
46684
+ }
46685
+ };
46686
+ var AsyncQuestionElicitations = class {
46687
+ constructor(sessionId, clientCapabilities, client) {
46688
+ this.sessionId = sessionId;
46689
+ this.clientCapabilities = clientCapabilities;
46690
+ this.client = client;
46691
+ }
46692
+ sessionId;
46693
+ clientCapabilities;
46694
+ client;
46695
+ // The running turns that asked a question, each with the signal that cancels its questions.
46696
+ turns = /* @__PURE__ */ new Map();
46697
+ /** Asks the questions of a completed async message and cancels them when their turn ends; never waits. */
46698
+ handleNotification(notification) {
46699
+ switch (notification.method) {
46700
+ case "item/completed": {
46701
+ const { threadId, turnId, item } = notification.params;
46702
+ if (threadId === this.sessionId && item.type === "agentMessage" && item.delivery === "async" && (item.questions?.length ?? 0) > 0) {
46703
+ this.ask(turnId, item);
46704
+ }
46705
+ return;
46706
+ }
46707
+ case "turn/completed":
46708
+ if (notification.params.threadId === this.sessionId) {
46709
+ this.endTurn(notification.params.turn.id);
46710
+ }
46711
+ return;
46712
+ default:
46713
+ return;
46714
+ }
46715
+ }
46716
+ /** Cancels every open question, when the session closes or its app-server is gone. */
46717
+ dispose() {
46718
+ for (const turnId of [...this.turns.keys()]) {
46719
+ this.endTurn(turnId);
46720
+ }
46721
+ }
46722
+ endTurn(turnId) {
46723
+ this.turns.get(turnId)?.abort();
46724
+ this.turns.delete(turnId);
46725
+ }
46726
+ ask(turnId, message) {
46727
+ if (!clientSupportsFormElicitation(this.clientCapabilities)) return;
46728
+ let turn = this.turns.get(turnId);
46729
+ if (turn === void 0) {
46730
+ turn = new AbortController();
46731
+ this.turns.set(turnId, turn);
46732
+ }
46733
+ const questions = formQuestions(message);
46734
+ const request = {
46735
+ sessionId: this.sessionId,
46736
+ mode: "form",
46737
+ message: "Codex keeps working while you answer. An answer sent after it finishes is not delivered.",
46738
+ // Questions are answered independently, as in Codex.
46739
+ requestedSchema: buildUserInputSchema(questions, this.clientCapabilities, false),
46740
+ _meta: { codex: { delivery: "async" } }
46741
+ };
46742
+ void this.client.createElicitation(request, turn.signal).then(
46743
+ (response) => this.answer(turnId, message, questions, response).catch((error62) => logger.error("Failed to deliver an async question answer", error62)),
46744
+ (error62) => logger.log("Async question closed without an answer", { sessionId: this.sessionId, turnId, error: error62 })
46745
+ );
46746
+ }
46747
+ async answer(turnId, message, questions, response) {
46748
+ const answers = userInputAnswers(response, questions, this.clientCapabilities);
46749
+ const replies = questions.flatMap((question, index) => {
46750
+ const answer = userInputAnswerText(answers[question.id]?.answers ?? []);
46751
+ return answer.length === 0 ? [] : [{
46752
+ questionItemId: asyncQuestionItemId(message.id, index),
46753
+ question: question.question,
46754
+ answer
46755
+ }];
46756
+ });
46757
+ if (replies.length === 0) return;
46758
+ if (this.turns.has(turnId)) {
46759
+ try {
46760
+ await this.client.steer(turnId, formatAsyncQuestionReply(replies));
46761
+ return;
46762
+ } catch (error62) {
46763
+ logger.log("Async question answer was not steered into its turn", { sessionId: this.sessionId, turnId, error: error62 });
46764
+ }
46765
+ }
46766
+ logger.log("Dropped an async question answer: its turn has ended", { sessionId: this.sessionId, turnId });
46767
+ await this.client.reportDroppedAnswer();
46768
+ }
46769
+ };
46770
+ function formQuestions(message) {
46771
+ return (message.questions ?? []).map((question, index) => {
46772
+ const options = question.options ?? [];
46773
+ return {
46774
+ id: `question_${index}`,
46775
+ header: "",
46776
+ question: question.title,
46777
+ // Codex always accepts an own answer.
46778
+ isOther: options.length > 0,
46779
+ isSecret: false,
46780
+ options: options.length > 0 ? options.map((label) => ({ label, description: "" })) : null
46781
+ };
46782
+ });
46783
+ }
46784
+
46570
46785
  // src/subagents/AcpSubagents.ts
46571
46786
  function subagentProtocol(capabilities, version2 = 1) {
46572
46787
  const subagents = capabilities?.subagents;
@@ -46622,7 +46837,7 @@ async function trustCodexHooks(client, cwd, requested) {
46622
46837
  }
46623
46838
 
46624
46839
  // src/CodexAcpServer.ts
46625
- import { randomUUID as randomUUID5 } from "node:crypto";
46840
+ import { randomUUID as randomUUID6 } from "node:crypto";
46626
46841
 
46627
46842
  // src/TitleGenerator.ts
46628
46843
  var TITLE_MODEL = "gpt-5.6-luna";
@@ -47685,7 +47900,7 @@ function remember(cache, key, value) {
47685
47900
  }
47686
47901
 
47687
47902
  // src/SessionListSubscriptions.ts
47688
- import { randomUUID as randomUUID4 } from "node:crypto";
47903
+ import { randomUUID as randomUUID5 } from "node:crypto";
47689
47904
  import path14 from "node:path";
47690
47905
 
47691
47906
  // src/CodexHomeWatcher.ts
@@ -48093,7 +48308,7 @@ var SessionListSubscriptions = class {
48093
48308
  this.startWatching();
48094
48309
  created.ready = this.makeReady(created);
48095
48310
  }
48096
- const subscription = { id: randomUUID4(), group, sent: /* @__PURE__ */ new Map(), ready: false, held: null, readyTimer: null, startedSecond: null, epoch: ++this.epoch };
48311
+ const subscription = { id: randomUUID5(), group, sent: /* @__PURE__ */ new Map(), ready: false, held: null, readyTimer: null, startedSecond: null, epoch: ++this.epoch };
48097
48312
  this.subscriptions.set(subscription.id, subscription);
48098
48313
  group.subscriptions.add(subscription);
48099
48314
  this.startWatching();
@@ -49464,6 +49679,8 @@ var CodexAcpServer = class {
49464
49679
  /** Tail of the per-session turn-start FIFO; see `acquireTurnStartReservation`. */
49465
49680
  turnStartQueueTail;
49466
49681
  steeringQueues;
49682
+ /** The async questions of each session, shown while the turn that asked them runs. */
49683
+ asyncQuestions = /* @__PURE__ */ new Map();
49467
49684
  /**
49468
49685
  * Steers awaiting their `userMessage` landing, keyed by the minted `clientUserMessageId`
49469
49686
  * passed to `turn/steer`. Used only to show a v2 live `user_message` when a steer injected
@@ -49555,7 +49772,7 @@ var CodexAcpServer = class {
49555
49772
  this.captureStderr();
49556
49773
  this.getExitCode = getExitCode ?? (() => this.codexProcessState?.connection.process.exitCode ?? null);
49557
49774
  this.getRecentStderr = getRecentStderr ?? (() => this.codexProcessState?.stderr ?? "");
49558
- this.sessionFailureEpoch = randomUUID5();
49775
+ this.sessionFailureEpoch = randomUUID6();
49559
49776
  this.clientInfo = null;
49560
49777
  this.clientCapabilities = null;
49561
49778
  this.capabilities = ClientCapabilities.DEFAULT;
@@ -50799,6 +51016,8 @@ You have been logged out. Please try again.`);
50799
51016
  } finally {
50800
51017
  if (this.getSessionGeneration(params.sessionId) === closeGeneration) {
50801
51018
  this.sessions.delete(params.sessionId);
51019
+ this.asyncQuestions.get(params.sessionId)?.dispose();
51020
+ this.asyncQuestions.delete(params.sessionId);
50802
51021
  if (!this.v2PromptsInFlight.has(params.sessionId)) {
50803
51022
  AcpV2Connection.of(this.connection)?.forgetSession(params.sessionId);
50804
51023
  }
@@ -51559,7 +51778,7 @@ You have been logged out. Please try again.`);
51559
51778
  });
51560
51779
  const sessionState = this.getSessionState(params.sessionId);
51561
51780
  this.assertSteerInputSupported(params, sessionState);
51562
- const clientUserMessageId = randomUUID5();
51781
+ const clientUserMessageId = randomUUID6();
51563
51782
  const turnId = await this.getSteerableTurnId(sessionState);
51564
51783
  if (turnId) {
51565
51784
  const injected = await this.injectSteerIntoActiveTurn(params, turnId, sessionState, clientUserMessageId);
@@ -52507,8 +52726,11 @@ You have been logged out. Please try again.`);
52507
52726
  }
52508
52727
  userInputToContentBlocks(input2) {
52509
52728
  switch (input2.type) {
52510
- case "text":
52729
+ case "text": {
52730
+ const asyncQuestionReply = renderAsyncQuestionReply(input2.text);
52731
+ if (asyncQuestionReply !== null) return [{ type: "text", text: asyncQuestionReply }];
52511
52732
  return desktopAttachmentHistory(input2.text) ?? (input2.text.length > 0 ? [{ type: "text", text: input2.text }] : []);
52733
+ }
52512
52734
  case "image":
52513
52735
  return [{
52514
52736
  type: "text",
@@ -52887,7 +53109,7 @@ You have been logged out. Please try again.`);
52887
53109
  }
52888
53110
  }
52889
53111
  const promptKind = this.availableCommands.classifyPrompt(request.prompt);
52890
- const messageId = randomUUID5();
53112
+ const messageId = randomUUID6();
52891
53113
  const session = new ACPSessionConnection(this.connection, sessionId);
52892
53114
  let failure3 = null;
52893
53115
  this.v2PromptsInFlight.add(sessionId);
@@ -53204,11 +53426,13 @@ You have been logged out. Please try again.`);
53204
53426
  await observeInteraction(event);
53205
53427
  if (!promptNotificationsActive) {
53206
53428
  await promptEventHandler.handleSessionScopedNotification(event);
53429
+ this.asyncQuestionsFor(sessionState).handleNotification(event);
53207
53430
  await this.trackCodexTurnCompletion(sessionState, event, promptEventHandler);
53208
53431
  return;
53209
53432
  }
53210
53433
  const completesActiveTurn = event.method === "turn/completed" && event.params.threadId === sessionState.sessionId && event.params.turn.id === sessionState.currentTurnId;
53211
53434
  await promptEventHandler.handleNotification(event);
53435
+ this.asyncQuestionsFor(sessionState).handleNotification(event);
53212
53436
  if (completesActiveTurn) {
53213
53437
  promptNotificationsActive = false;
53214
53438
  }
@@ -53440,7 +53664,7 @@ You have been logged out. Please try again.`);
53440
53664
  activePrompt.currentTurn = null;
53441
53665
  sessionState.currentTurnId = null;
53442
53666
  sessionState.interruptTurnId = null;
53443
- const implementationClientUserMessageId = insertion !== void 0 ? randomUUID5() : void 0;
53667
+ const implementationClientUserMessageId = insertion !== void 0 ? randomUUID6() : void 0;
53444
53668
  if (implementationClientUserMessageId !== void 0) {
53445
53669
  registerSyntheticInsertion(implementationClientUserMessageId, implementationRequest.prompt);
53446
53670
  }
@@ -53651,12 +53875,28 @@ You have been logged out. Please try again.`);
53651
53875
  if (this.recovery === null || !lost.aborted || !sessionState.subagents.hasOutstanding()) return;
53652
53876
  throw this.recovery.errorForGeneration(generation);
53653
53877
  }
53878
+ /** The async questions of a session, created on first use. */
53879
+ asyncQuestionsFor(sessionState) {
53880
+ const sessionId = sessionState.sessionId;
53881
+ let questions = this.asyncQuestions.get(sessionId);
53882
+ if (questions === void 0) {
53883
+ questions = new AsyncQuestionElicitations(sessionId, this.clientCapabilities, new AsyncQuestionClient(
53884
+ sessionId,
53885
+ this.connection,
53886
+ this.clientCapabilities,
53887
+ () => this.codexAcpClient
53888
+ ));
53889
+ this.asyncQuestions.set(sessionId, questions);
53890
+ }
53891
+ return questions;
53892
+ }
53654
53893
  /**
53655
53894
  * Ends what a prompt showed as running when its app-server died: open tool calls of the session and of its native
53656
53895
  * subagent sessions, the subagent sessions themselves and compactions. Nothing will ever complete them.
53657
53896
  */
53658
53897
  async finishPromptAfterAppServerLoss(sessionState, eventHandler, promptClient, elicitations, state) {
53659
53898
  try {
53899
+ this.asyncQuestions.get(sessionState.sessionId)?.dispose();
53660
53900
  await elicitations?.completeAllUrlElicitations();
53661
53901
  const sessionIds = /* @__PURE__ */ new Set([sessionState.sessionId, ...sessionState.subagents.childSessionIds()]);
53662
53902
  await this.failUnfinishedToolCalls(sessionIds);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "2.5.0",
6
+ "version": "2.5.1-preview.2",
7
7
  "description": "",
8
8
  "main": "dist/index.js",
9
9
  "bin": {