@alas-ide/codex-acp 2.5.0-alas.1 → 2.5.0-alas.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 +366 -126
  3. package/package.json +2 -2
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
@@ -32389,8 +32389,19 @@ var AcpV2Connection = class {
32389
32389
  return toV1RequestPermissionResponse(response);
32390
32390
  }, options?.cancellationSignal);
32391
32391
  }
32392
- /** Request-scoped login elicitations and idle background elicitations never change state. */
32393
- async createElicitation(request, options) {
32392
+ /**
32393
+ * Sends `elicitation/create`. Request-scoped login elicitations and idle background elicitations never change
32394
+ * the session state.
32395
+ *
32396
+ * @param request The elicitation to show.
32397
+ * @param options Request options, such as the signal that cancels the request.
32398
+ * @param interaction.userAction Whether the agent waits for the answer, `true` by default. While such an
32399
+ * elicitation is open in a running turn, the session reports `requires_action` whenever none of its items
32400
+ * is in progress. Pass `false` for a question the agent keeps working through, so the session state is left
32401
+ * as it is.
32402
+ */
32403
+ async createElicitation(request, options, interaction = {}) {
32404
+ const userAction = interaction.userAction ?? true;
32394
32405
  const sendRequest = async () => {
32395
32406
  const response = await this.client.request(
32396
32407
  methods2.client.elicitation.create,
@@ -32400,7 +32411,7 @@ var AcpV2Connection = class {
32400
32411
  return toV1CreateElicitationResponse(response);
32401
32412
  };
32402
32413
  const sessionId = elicitationSessionId(request);
32403
- if (sessionId === void 0) {
32414
+ if (sessionId === void 0 || !userAction) {
32404
32415
  return sendRequest();
32405
32416
  }
32406
32417
  const toolCallId = "toolCallId" in request && typeof request.toolCallId === "string" ? request.toolCallId : void 0;
@@ -37649,12 +37660,16 @@ ${params.turnId}`;
37649
37660
  }
37650
37661
  return this.createAgentMessageText(citations.push(event.delta), event.itemId, event.threadId);
37651
37662
  }
37652
- /** Sends the text a file citation held back, once the message is complete. */
37653
- flushAgentMessageText(itemId, threadId) {
37663
+ /**
37664
+ * Sends the text a file citation held back, once the message is complete. A message that
37665
+ * arrives whole, without deltas (an async question, for one), is sent in full.
37666
+ */
37667
+ flushAgentMessageText(item, threadId) {
37654
37668
  const streams = this.streamsFor(threadId);
37655
- const rest = streams.agentMessageCitations.get(itemId)?.flush() ?? "";
37656
- streams.agentMessageCitations.delete(itemId);
37657
- return this.createAgentMessageText(rest, itemId, threadId);
37669
+ const citations = streams.agentMessageCitations.get(item.id);
37670
+ streams.agentMessageCitations.delete(item.id);
37671
+ const text = citations ? citations.flush() : renderFileCitations(item.text, this.sessionState.cwd);
37672
+ return this.createAgentMessageText(text, item.id, threadId);
37658
37673
  }
37659
37674
  createAgentMessageText(text, itemId, threadId) {
37660
37675
  if (text.length === 0) return null;
@@ -37809,7 +37824,7 @@ ${event.details}` : event.summary;
37809
37824
  return this.renderer.render(this.subagents.legacyCollaborationCompleted(event.item));
37810
37825
  case "agentMessage":
37811
37826
  this.rememberAgentMessagePhase(event.item, event.threadId);
37812
- return this.flushAgentMessageText(event.item.id, event.threadId);
37827
+ return this.flushAgentMessageText(event.item, event.threadId);
37813
37828
  case "plan":
37814
37829
  return await this.createCompletedPlanEvent(event.item);
37815
37830
  case "exitedReviewMode":
@@ -38989,6 +39004,119 @@ function userInputResponseValue(content, fieldId) {
38989
39004
  }
38990
39005
  return value;
38991
39006
  }
39007
+ function buildUserInputSchema(questions, clientCapabilities, answersRequired) {
39008
+ const properties = {};
39009
+ const required2 = [];
39010
+ const questionIds = new Set(questions.map((question) => question.id));
39011
+ const airClient = isAirClient(clientCapabilities);
39012
+ for (const question of questions) {
39013
+ const options = question.options ?? [];
39014
+ const hasOptions = options.length > 0;
39015
+ const hasOtherAnswer = question.isOther && hasOptions;
39016
+ const base = {
39017
+ title: question.question || question.header || question.id,
39018
+ ...question.header ? { description: question.header } : {},
39019
+ _meta: {
39020
+ codex: {
39021
+ isOther: question.isOther,
39022
+ isSecret: question.isSecret
39023
+ }
39024
+ }
39025
+ };
39026
+ if (answersRequired) {
39027
+ required2.push(question.id);
39028
+ }
39029
+ properties[question.id] = hasOptions ? {
39030
+ ...base,
39031
+ type: "string",
39032
+ oneOf: [
39033
+ ...options.map((option) => ({
39034
+ const: option.label,
39035
+ title: option.label,
39036
+ ...option.description ? { description: option.description } : {}
39037
+ })),
39038
+ ...hasOtherAnswer && !airClient && !options.some((option) => option.label === USER_INPUT_OTHER_OPTION) ? [{
39039
+ const: USER_INPUT_OTHER_OPTION,
39040
+ title: USER_INPUT_OTHER_OPTION,
39041
+ description: "Provide a different answer in the note field."
39042
+ }] : []
39043
+ ]
39044
+ } : {
39045
+ ...base,
39046
+ type: "string"
39047
+ };
39048
+ if (hasOtherAnswer) {
39049
+ const noteMeta = {
39050
+ codex: {
39051
+ questionId: question.id,
39052
+ role: "user_note",
39053
+ isSecret: question.isSecret
39054
+ }
39055
+ };
39056
+ properties[userInputNoteFieldId(question.id, questionIds)] = {
39057
+ type: "string",
39058
+ title: "Additional answer or note",
39059
+ _meta: airClient ? withAirMeta({ ...noteMeta, [LEGACY_AIR_CUSTOM_ANSWER_KEY]: true }, AIR_CUSTOM_ANSWER_KEY, true) : noteMeta
39060
+ };
39061
+ }
39062
+ }
39063
+ return {
39064
+ type: "object",
39065
+ properties,
39066
+ ...answersRequired ? { required: required2 } : {}
39067
+ };
39068
+ }
39069
+ function buildUserInputRequest(params, clientCapabilities) {
39070
+ return {
39071
+ sessionId: params.threadId,
39072
+ toolCallId: params.itemId,
39073
+ mode: "form",
39074
+ message: "Codex needs your input to continue.",
39075
+ requestedSchema: buildUserInputSchema(params.questions, clientCapabilities, true),
39076
+ _meta: {
39077
+ codex: {
39078
+ autoResolutionMs: params.autoResolutionMs
39079
+ }
39080
+ }
39081
+ };
39082
+ }
39083
+ function userInputAnswers(response, questions, clientCapabilities) {
39084
+ if (!CreateElicitationResponse.isAccept(response)) {
39085
+ return {};
39086
+ }
39087
+ const answers = {};
39088
+ const content = contentRecord(response.content);
39089
+ const questionIds = new Set(questions.map((question) => question.id));
39090
+ const airClient = isAirClient(clientCapabilities);
39091
+ for (const question of questions) {
39092
+ const answerValues = [];
39093
+ const hasOtherAnswer = question.isOther && question.options != null && question.options.length > 0;
39094
+ const value = userInputResponseValue(content, question.id);
39095
+ const typedAnswer = airClient && hasOtherAnswer ? typedChoiceAnswer(value, question) : void 0;
39096
+ if (typedAnswer !== void 0) {
39097
+ answerValues.push(USER_INPUT_OTHER_OPTION, `${USER_INPUT_NOTE_PREFIX}${typedAnswer.trim()}`);
39098
+ } else if (value !== void 0) {
39099
+ answerValues.push(...Array.isArray(value) ? value.map(String) : [String(value)]);
39100
+ }
39101
+ if (hasOtherAnswer) {
39102
+ const note = userInputResponseValue(content, userInputNoteFieldId(question.id, questionIds));
39103
+ if (note !== void 0) {
39104
+ const notes = Array.isArray(note) ? note : [note];
39105
+ answerValues.push(...notes.map((item) => `${USER_INPUT_NOTE_PREFIX}${String(item).trim()}`));
39106
+ }
39107
+ }
39108
+ if (answerValues.length === 0) {
39109
+ continue;
39110
+ }
39111
+ answers[question.id] = {
39112
+ answers: answerValues
39113
+ };
39114
+ }
39115
+ return answers;
39116
+ }
39117
+ function userInputAnswerText(answers) {
39118
+ 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");
39119
+ }
38992
39120
  var CodexElicitationHandler = class {
38993
39121
  connection;
38994
39122
  renderer;
@@ -39097,7 +39225,7 @@ var CodexElicitationHandler = class {
39097
39225
  if (response === null) {
39098
39226
  return { answers: {} };
39099
39227
  }
39100
- return this.convertUserInputResponse(response, params);
39228
+ return { answers: userInputAnswers(response, params.questions, this.clientCapabilities) };
39101
39229
  } catch (error62) {
39102
39230
  logger.error("Error handling Codex user input request", error62);
39103
39231
  return { answers: {} };
@@ -39107,7 +39235,7 @@ var CodexElicitationHandler = class {
39107
39235
  return cancellationSignal ? { cancellationSignal } : void 0;
39108
39236
  }
39109
39237
  async requestUserInputElicitation(params) {
39110
- const request = this.buildUserInputRequest(params);
39238
+ const request = buildUserInputRequest(params, this.clientCapabilities);
39111
39239
  const signal = subagentInteractionSignal(params.threadId, this.cancellationSignal);
39112
39240
  if (params.autoResolutionMs === null) {
39113
39241
  return await withSubagentUserAction(params.threadId, () => this.connection.request(
@@ -39207,77 +39335,6 @@ var CodexElicitationHandler = class {
39207
39335
  };
39208
39336
  }
39209
39337
  }
39210
- buildUserInputRequest(params) {
39211
- const properties = {};
39212
- const required2 = [];
39213
- const questionIds = new Set(params.questions.map((question) => question.id));
39214
- const airClient = isAirClient(this.clientCapabilities);
39215
- for (const question of params.questions) {
39216
- const options = question.options ?? [];
39217
- const hasOptions = options.length > 0;
39218
- const hasOtherAnswer = question.isOther && hasOptions;
39219
- const base = {
39220
- title: question.question || question.header || question.id,
39221
- ...question.header ? { description: question.header } : {},
39222
- _meta: {
39223
- codex: {
39224
- isOther: question.isOther,
39225
- isSecret: question.isSecret
39226
- }
39227
- }
39228
- };
39229
- required2.push(question.id);
39230
- properties[question.id] = hasOptions ? {
39231
- ...base,
39232
- type: "string",
39233
- oneOf: [
39234
- ...options.map((option) => ({
39235
- const: option.label,
39236
- title: option.label,
39237
- ...option.description ? { description: option.description } : {}
39238
- })),
39239
- ...hasOtherAnswer && !airClient && !options.some((option) => option.label === USER_INPUT_OTHER_OPTION) ? [{
39240
- const: USER_INPUT_OTHER_OPTION,
39241
- title: USER_INPUT_OTHER_OPTION,
39242
- description: "Provide a different answer in the note field."
39243
- }] : []
39244
- ]
39245
- } : {
39246
- ...base,
39247
- type: "string"
39248
- };
39249
- if (hasOtherAnswer) {
39250
- const noteMeta = {
39251
- codex: {
39252
- questionId: question.id,
39253
- role: "user_note",
39254
- isSecret: question.isSecret
39255
- }
39256
- };
39257
- properties[userInputNoteFieldId(question.id, questionIds)] = {
39258
- type: "string",
39259
- title: "Additional answer or note",
39260
- _meta: airClient ? withAirMeta({ ...noteMeta, [LEGACY_AIR_CUSTOM_ANSWER_KEY]: true }, AIR_CUSTOM_ANSWER_KEY, true) : noteMeta
39261
- };
39262
- }
39263
- }
39264
- return {
39265
- sessionId: params.threadId,
39266
- toolCallId: params.itemId,
39267
- mode: "form",
39268
- message: "Codex needs your input to continue.",
39269
- requestedSchema: {
39270
- type: "object",
39271
- properties,
39272
- required: required2
39273
- },
39274
- _meta: {
39275
- codex: {
39276
- autoResolutionMs: params.autoResolutionMs
39277
- }
39278
- }
39279
- };
39280
- }
39281
39338
  convertElicitationResponse(response, context) {
39282
39339
  if (CreateElicitationResponse.isAccept(response)) {
39283
39340
  const content = contentRecord(response.content);
@@ -39308,40 +39365,6 @@ var CodexElicitationHandler = class {
39308
39365
  isAllowedToolApprovalPersist(persist, persistOptions) {
39309
39366
  return persist === void 0 || persist === "once" || persist === "session" && persistOptions.has("session") || persist === "always" && persistOptions.has("always");
39310
39367
  }
39311
- convertUserInputResponse(response, params) {
39312
- if (!CreateElicitationResponse.isAccept(response)) {
39313
- return { answers: {} };
39314
- }
39315
- const answers = {};
39316
- const content = contentRecord(response.content);
39317
- const questionIds = new Set(params.questions.map((question) => question.id));
39318
- const airClient = isAirClient(this.clientCapabilities);
39319
- for (const question of params.questions) {
39320
- const answerValues = [];
39321
- const hasOtherAnswer = question.isOther && question.options != null && question.options.length > 0;
39322
- const value = userInputResponseValue(content, question.id);
39323
- const typedAnswer = airClient && hasOtherAnswer ? typedChoiceAnswer(value, question) : void 0;
39324
- if (typedAnswer !== void 0) {
39325
- answerValues.push(USER_INPUT_OTHER_OPTION, `${USER_INPUT_NOTE_PREFIX}${typedAnswer.trim()}`);
39326
- } else if (value !== void 0) {
39327
- answerValues.push(...Array.isArray(value) ? value.map(String) : [String(value)]);
39328
- }
39329
- if (hasOtherAnswer) {
39330
- const note = userInputResponseValue(content, userInputNoteFieldId(question.id, questionIds));
39331
- if (note !== void 0) {
39332
- const notes = Array.isArray(note) ? note : [note];
39333
- answerValues.push(...notes.map((item) => `${USER_INPUT_NOTE_PREFIX}${String(item).trim()}`));
39334
- }
39335
- }
39336
- if (answerValues.length === 0) {
39337
- continue;
39338
- }
39339
- answers[question.id] = {
39340
- answers: answerValues
39341
- };
39342
- }
39343
- return { answers };
39344
- }
39345
39368
  async publishAcceptedMcpToolApproval(sessionId, context, accepted) {
39346
39369
  if (!accepted || context.correlatedCallId === void 0) {
39347
39370
  return;
@@ -40588,7 +40611,7 @@ var package_default = {
40588
40611
  publishConfig: {
40589
40612
  access: "public"
40590
40613
  },
40591
- version: "2.5.0-alas.1",
40614
+ version: "2.5.0-alas.2",
40592
40615
  description: "",
40593
40616
  main: "dist/index.js",
40594
40617
  bin: {
@@ -40666,7 +40689,7 @@ var package_default = {
40666
40689
  alasDownstream: {
40667
40690
  upstreamVersion: "2.5.0",
40668
40691
  upstreamCommit: "34e08970aa324a2be985f5080674d5aa91d7666f",
40669
- sourceCommit: "845c14e611fff5b534ecd38847d0b3dc1b69d0e6"
40692
+ sourceCommit: "0f50c0b0345fd0e67b91ffc3c49d6b998d27da0c"
40670
40693
  }
40671
40694
  };
40672
40695
 
@@ -48951,6 +48974,198 @@ function isJetBrains2026_1Client(clientInfo) {
48951
48974
  return (isIntelliJPlatform || isJetBrainsClient) && clientInfo.version.startsWith("2026.1");
48952
48975
  }
48953
48976
 
48977
+ // src/CodexAsyncQuestions.ts
48978
+ import { randomUUID as randomUUID5 } from "node:crypto";
48979
+ var TOOL_NAME = "request_user_input_async";
48980
+ var REPLY_START = "<send_user_message_question_reply>";
48981
+ var REPLY_END = "</send_user_message_question_reply>";
48982
+ var MAX_REPLY_FIELD_LENGTH = 512;
48983
+ function asyncQuestionItemId(messageId, index) {
48984
+ return JSON.stringify([TOOL_NAME, messageId, index]);
48985
+ }
48986
+ function formatAsyncQuestionReply(replies) {
48987
+ const bounded = replies.map((reply) => ({
48988
+ ...reply,
48989
+ question: [...reply.question].slice(0, MAX_REPLY_FIELD_LENGTH).join("").replace(/[\r\n]/g, " ")
48990
+ }));
48991
+ if (bounded.some((reply) => reply.questionItemId.length > MAX_REPLY_FIELD_LENGTH)) {
48992
+ return displayReplies(bounded);
48993
+ }
48994
+ const body = bounded.map(({ answer, question, questionItemId }) => ({ answer, question, questionItemId }));
48995
+ return `${REPLY_START}
48996
+ ${JSON.stringify(body)}
48997
+ ${REPLY_END}`;
48998
+ }
48999
+ function parseAsyncQuestionReply(text) {
49000
+ const trimmed = text.trim();
49001
+ if (!trimmed.startsWith(REPLY_START) || !trimmed.endsWith(REPLY_END)) return null;
49002
+ let body;
49003
+ try {
49004
+ body = JSON.parse(trimmed.slice(REPLY_START.length, trimmed.length - REPLY_END.length));
49005
+ } catch {
49006
+ return null;
49007
+ }
49008
+ const replies = Array.isArray(body) ? body : [body];
49009
+ if (replies.length === 0 || !replies.every(isReply)) return null;
49010
+ return replies.map(({ questionItemId, question, answer }) => ({ questionItemId, question, answer }));
49011
+ }
49012
+ function renderAsyncQuestionReply(text) {
49013
+ const replies = parseAsyncQuestionReply(text);
49014
+ return replies === null ? null : displayReplies(replies);
49015
+ }
49016
+ function displayReplies(replies) {
49017
+ return replies.map((reply) => `> ${reply.question}
49018
+
49019
+ ${reply.answer}`).join("\n\n");
49020
+ }
49021
+ function isReply(value) {
49022
+ if (typeof value !== "object" || value === null) return false;
49023
+ const reply = value;
49024
+ return typeof reply["questionItemId"] === "string" && typeof reply["question"] === "string" && typeof reply["answer"] === "string";
49025
+ }
49026
+ var AsyncQuestionClient = class {
49027
+ /**
49028
+ * @param sessionId The session that the questions belong to.
49029
+ * @param connection The connection to the ACP client.
49030
+ * @param clientCapabilities The capabilities that the ACP client declared.
49031
+ * @param codexAcpClient Returns the current Codex client, which changes when the app-server restarts.
49032
+ */
49033
+ constructor(sessionId, connection, clientCapabilities, codexAcpClient) {
49034
+ this.sessionId = sessionId;
49035
+ this.connection = connection;
49036
+ this.clientCapabilities = clientCapabilities;
49037
+ this.codexAcpClient = codexAcpClient;
49038
+ }
49039
+ sessionId;
49040
+ connection;
49041
+ clientCapabilities;
49042
+ codexAcpClient;
49043
+ /** Sends the elicitation without marking the session as waiting for the user. */
49044
+ createElicitation(request, signal) {
49045
+ const v2 = AcpV2Connection.of(this.connection);
49046
+ return v2 !== null ? v2.createElicitation(request, { cancellationSignal: signal }, { userAction: false }) : this.connection.request(methods.client.elicitation.create, request, { cancellationSignal: signal });
49047
+ }
49048
+ /** Steers the text into the turn; rejects if that turn is no longer running. */
49049
+ async steer(turnId, text) {
49050
+ await this.codexAcpClient().steerTurn({
49051
+ threadId: this.sessionId,
49052
+ turnId,
49053
+ prompt: [{ type: "text", text }],
49054
+ clientUserMessageId: randomUUID5()
49055
+ });
49056
+ }
49057
+ /** Tells the user that an answer was not sent, if the client supports notices. */
49058
+ async reportDroppedAnswer() {
49059
+ if (!clientSupportsNotices(this.clientCapabilities)) return;
49060
+ await this.connection.notify(methods.client.session.update, {
49061
+ sessionId: this.sessionId,
49062
+ update: createSessionNotice(
49063
+ "warning",
49064
+ "Answer not sent",
49065
+ "Codex finished before your answer arrived. Send it as a new message if it still matters."
49066
+ )
49067
+ });
49068
+ }
49069
+ };
49070
+ var AsyncQuestionElicitations = class {
49071
+ constructor(sessionId, clientCapabilities, client) {
49072
+ this.sessionId = sessionId;
49073
+ this.clientCapabilities = clientCapabilities;
49074
+ this.client = client;
49075
+ }
49076
+ sessionId;
49077
+ clientCapabilities;
49078
+ client;
49079
+ // The running turns that asked a question, each with the signal that cancels its questions.
49080
+ turns = /* @__PURE__ */ new Map();
49081
+ /** Asks the questions of a completed async message and cancels them when their turn ends; never waits. */
49082
+ handleNotification(notification) {
49083
+ switch (notification.method) {
49084
+ case "item/completed": {
49085
+ const { threadId, turnId, item } = notification.params;
49086
+ if (threadId === this.sessionId && item.type === "agentMessage" && item.delivery === "async" && (item.questions?.length ?? 0) > 0) {
49087
+ this.ask(turnId, item);
49088
+ }
49089
+ return;
49090
+ }
49091
+ case "turn/completed":
49092
+ if (notification.params.threadId === this.sessionId) {
49093
+ this.endTurn(notification.params.turn.id);
49094
+ }
49095
+ return;
49096
+ default:
49097
+ return;
49098
+ }
49099
+ }
49100
+ /** Cancels every open question, when the session closes or its app-server is gone. */
49101
+ dispose() {
49102
+ for (const turnId of [...this.turns.keys()]) {
49103
+ this.endTurn(turnId);
49104
+ }
49105
+ }
49106
+ endTurn(turnId) {
49107
+ this.turns.get(turnId)?.abort();
49108
+ this.turns.delete(turnId);
49109
+ }
49110
+ ask(turnId, message) {
49111
+ if (!clientSupportsFormElicitation(this.clientCapabilities)) return;
49112
+ let turn = this.turns.get(turnId);
49113
+ if (turn === void 0) {
49114
+ turn = new AbortController();
49115
+ this.turns.set(turnId, turn);
49116
+ }
49117
+ const questions = formQuestions(message);
49118
+ const request = {
49119
+ sessionId: this.sessionId,
49120
+ mode: "form",
49121
+ message: "Codex keeps working while you answer. An answer sent after it finishes is not delivered.",
49122
+ // Questions are answered independently, as in Codex.
49123
+ requestedSchema: buildUserInputSchema(questions, this.clientCapabilities, false),
49124
+ _meta: { codex: { delivery: "async" } }
49125
+ };
49126
+ void this.client.createElicitation(request, turn.signal).then(
49127
+ (response) => this.answer(turnId, message, questions, response).catch((error62) => logger.error("Failed to deliver an async question answer", error62)),
49128
+ (error62) => logger.log("Async question closed without an answer", { sessionId: this.sessionId, turnId, error: error62 })
49129
+ );
49130
+ }
49131
+ async answer(turnId, message, questions, response) {
49132
+ const answers = userInputAnswers(response, questions, this.clientCapabilities);
49133
+ const replies = questions.flatMap((question, index) => {
49134
+ const answer = userInputAnswerText(answers[question.id]?.answers ?? []);
49135
+ return answer.length === 0 ? [] : [{
49136
+ questionItemId: asyncQuestionItemId(message.id, index),
49137
+ question: question.question,
49138
+ answer
49139
+ }];
49140
+ });
49141
+ if (replies.length === 0) return;
49142
+ if (this.turns.has(turnId)) {
49143
+ try {
49144
+ await this.client.steer(turnId, formatAsyncQuestionReply(replies));
49145
+ return;
49146
+ } catch (error62) {
49147
+ logger.log("Async question answer was not steered into its turn", { sessionId: this.sessionId, turnId, error: error62 });
49148
+ }
49149
+ }
49150
+ logger.log("Dropped an async question answer: its turn has ended", { sessionId: this.sessionId, turnId });
49151
+ await this.client.reportDroppedAnswer();
49152
+ }
49153
+ };
49154
+ function formQuestions(message) {
49155
+ return (message.questions ?? []).map((question, index) => {
49156
+ const options = question.options ?? [];
49157
+ return {
49158
+ id: `question_${index}`,
49159
+ header: "",
49160
+ question: question.title,
49161
+ // Codex always accepts an own answer.
49162
+ isOther: options.length > 0,
49163
+ isSecret: false,
49164
+ options: options.length > 0 ? options.map((label) => ({ label, description: "" })) : null
49165
+ };
49166
+ });
49167
+ }
49168
+
48954
49169
  // src/subagents/AcpSubagents.ts
48955
49170
  function subagentProtocol(capabilities, version2 = 1) {
48956
49171
  const subagents = capabilities?.subagents;
@@ -48979,7 +49194,7 @@ var SubagentInteractionHandlers = class {
48979
49194
  };
48980
49195
 
48981
49196
  // src/CodexAcpServer.ts
48982
- import { randomUUID as randomUUID6 } from "node:crypto";
49197
+ import { randomUUID as randomUUID7 } from "node:crypto";
48983
49198
 
48984
49199
  // src/TitleGenerator.ts
48985
49200
  var TITLE_MODEL = "gpt-5.6-luna";
@@ -50067,7 +50282,7 @@ function remember(cache, key, value) {
50067
50282
  }
50068
50283
 
50069
50284
  // src/SessionListSubscriptions.ts
50070
- import { randomUUID as randomUUID5 } from "node:crypto";
50285
+ import { randomUUID as randomUUID6 } from "node:crypto";
50071
50286
  import path14 from "node:path";
50072
50287
 
50073
50288
  // src/CodexHomeWatcher.ts
@@ -50475,7 +50690,7 @@ var SessionListSubscriptions = class {
50475
50690
  this.startWatching();
50476
50691
  created.ready = this.makeReady(created);
50477
50692
  }
50478
- const subscription = { id: randomUUID5(), group, sent: /* @__PURE__ */ new Map(), ready: false, held: null, readyTimer: null, startedSecond: null, epoch: ++this.epoch };
50693
+ const subscription = { id: randomUUID6(), group, sent: /* @__PURE__ */ new Map(), ready: false, held: null, readyTimer: null, startedSecond: null, epoch: ++this.epoch };
50479
50694
  this.subscriptions.set(subscription.id, subscription);
50480
50695
  group.subscriptions.add(subscription);
50481
50696
  this.startWatching();
@@ -51867,6 +52082,8 @@ var CodexAcpServer = class {
51867
52082
  /** Tail of the per-session turn-start FIFO; see `acquireTurnStartReservation`. */
51868
52083
  turnStartQueueTail;
51869
52084
  steeringQueues;
52085
+ /** The async questions of each session, shown while the turn that asked them runs. */
52086
+ asyncQuestions = /* @__PURE__ */ new Map();
51870
52087
  /**
51871
52088
  * Steers awaiting their `userMessage` landing, keyed by the minted `clientUserMessageId`
51872
52089
  * passed to `turn/steer`. Used only to show a v2 live `user_message` when a steer injected
@@ -51959,7 +52176,7 @@ var CodexAcpServer = class {
51959
52176
  this.captureStderr();
51960
52177
  this.getExitCode = getExitCode ?? (() => this.codexProcessState?.connection.process.exitCode ?? null);
51961
52178
  this.getRecentStderr = getRecentStderr ?? (() => this.codexProcessState?.stderr ?? "");
51962
- this.sessionFailureEpoch = randomUUID6();
52179
+ this.sessionFailureEpoch = randomUUID7();
51963
52180
  this.clientInfo = null;
51964
52181
  this.clientCapabilities = null;
51965
52182
  this.capabilities = ClientCapabilities.DEFAULT;
@@ -53252,6 +53469,8 @@ You have been logged out. Please try again.`);
53252
53469
  } finally {
53253
53470
  if (this.getSessionGeneration(params.sessionId) === closeGeneration) {
53254
53471
  this.sessions.delete(params.sessionId);
53472
+ this.asyncQuestions.get(params.sessionId)?.dispose();
53473
+ this.asyncQuestions.delete(params.sessionId);
53255
53474
  if (!this.v2PromptsInFlight.has(params.sessionId)) {
53256
53475
  AcpV2Connection.of(this.connection)?.forgetSession(params.sessionId);
53257
53476
  }
@@ -54028,7 +54247,7 @@ You have been logged out. Please try again.`);
54028
54247
  });
54029
54248
  const sessionState = this.getSessionState(params.sessionId);
54030
54249
  this.assertSteerInputSupported(params, sessionState);
54031
- const clientUserMessageId = randomUUID6();
54250
+ const clientUserMessageId = randomUUID7();
54032
54251
  const turnId = await this.getSteerableTurnId(sessionState);
54033
54252
  if (turnId) {
54034
54253
  const injected = await this.injectSteerIntoActiveTurn(params, turnId, sessionState, clientUserMessageId);
@@ -55059,8 +55278,11 @@ You have been logged out. Please try again.`);
55059
55278
  }
55060
55279
  userInputToContentBlocks(input2) {
55061
55280
  switch (input2.type) {
55062
- case "text":
55281
+ case "text": {
55282
+ const asyncQuestionReply = renderAsyncQuestionReply(input2.text);
55283
+ if (asyncQuestionReply !== null) return [{ type: "text", text: asyncQuestionReply }];
55063
55284
  return desktopAttachmentHistory(input2.text) ?? (input2.text.length > 0 ? [{ type: "text", text: input2.text }] : []);
55285
+ }
55064
55286
  case "image":
55065
55287
  return [{
55066
55288
  type: "text",
@@ -55439,7 +55661,7 @@ You have been logged out. Please try again.`);
55439
55661
  }
55440
55662
  }
55441
55663
  const promptKind = this.availableCommands.classifyPrompt(request.prompt);
55442
- const messageId = randomUUID6();
55664
+ const messageId = randomUUID7();
55443
55665
  const session = new ACPSessionConnection(this.connection, sessionId);
55444
55666
  let failure3 = null;
55445
55667
  this.v2PromptsInFlight.add(sessionId);
@@ -55763,11 +55985,13 @@ You have been logged out. Please try again.`);
55763
55985
  await observeInteraction(event);
55764
55986
  if (!promptNotificationsActive) {
55765
55987
  await promptEventHandler.handleSessionScopedNotification(event);
55988
+ this.asyncQuestionsFor(sessionState).handleNotification(event);
55766
55989
  await this.trackCodexTurnCompletion(sessionState, event, promptEventHandler);
55767
55990
  return;
55768
55991
  }
55769
55992
  const completesActiveTurn = event.method === "turn/completed" && event.params.threadId === sessionState.sessionId && event.params.turn.id === sessionState.currentTurnId;
55770
55993
  await promptEventHandler.handleNotification(event);
55994
+ this.asyncQuestionsFor(sessionState).handleNotification(event);
55771
55995
  if (completesActiveTurn) {
55772
55996
  promptNotificationsActive = false;
55773
55997
  }
@@ -56000,7 +56224,7 @@ You have been logged out. Please try again.`);
56000
56224
  activePrompt.currentTurn = null;
56001
56225
  sessionState.currentTurnId = null;
56002
56226
  sessionState.interruptTurnId = null;
56003
- const implementationClientUserMessageId = insertion !== void 0 ? randomUUID6() : void 0;
56227
+ const implementationClientUserMessageId = insertion !== void 0 ? randomUUID7() : void 0;
56004
56228
  if (implementationClientUserMessageId !== void 0) {
56005
56229
  registerSyntheticInsertion(implementationClientUserMessageId, implementationRequest.prompt);
56006
56230
  }
@@ -56211,12 +56435,28 @@ You have been logged out. Please try again.`);
56211
56435
  if (this.recovery === null || !lost.aborted || !sessionState.subagents.hasOutstanding()) return;
56212
56436
  throw this.recovery.errorForGeneration(generation);
56213
56437
  }
56438
+ /** The async questions of a session, created on first use. */
56439
+ asyncQuestionsFor(sessionState) {
56440
+ const sessionId = sessionState.sessionId;
56441
+ let questions = this.asyncQuestions.get(sessionId);
56442
+ if (questions === void 0) {
56443
+ questions = new AsyncQuestionElicitations(sessionId, this.clientCapabilities, new AsyncQuestionClient(
56444
+ sessionId,
56445
+ this.connection,
56446
+ this.clientCapabilities,
56447
+ () => this.codexAcpClient
56448
+ ));
56449
+ this.asyncQuestions.set(sessionId, questions);
56450
+ }
56451
+ return questions;
56452
+ }
56214
56453
  /**
56215
56454
  * Ends what a prompt showed as running when its app-server died: open tool calls of the session and of its native
56216
56455
  * subagent sessions, the subagent sessions themselves and compactions. Nothing will ever complete them.
56217
56456
  */
56218
56457
  async finishPromptAfterAppServerLoss(sessionState, eventHandler, promptClient, elicitations, state) {
56219
56458
  try {
56459
+ this.asyncQuestions.get(sessionState.sessionId)?.dispose();
56220
56460
  await elicitations?.completeAllUrlElicitations();
56221
56461
  const sessionIds = /* @__PURE__ */ new Set([sessionState.sessionId, ...sessionState.subagents.childSessionIds()]);
56222
56462
  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-alas.1",
6
+ "version": "2.5.0-alas.2",
7
7
  "description": "",
8
8
  "main": "dist/index.js",
9
9
  "bin": {
@@ -81,6 +81,6 @@
81
81
  "alasDownstream": {
82
82
  "upstreamVersion": "2.5.0",
83
83
  "upstreamCommit": "34e08970aa324a2be985f5080674d5aa91d7666f",
84
- "sourceCommit": "845c14e611fff5b534ecd38847d0b3dc1b69d0e6"
84
+ "sourceCommit": "0f50c0b0345fd0e67b91ffc3c49d6b998d27da0c"
85
85
  }
86
86
  }