@agentclientprotocol/codex-acp 1.6.0 → 1.7.0

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 +9 -2
  2. package/dist/index.js +1962 -1005
  3. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -3453,7 +3453,7 @@ var require_main = __commonJS({
3453
3453
  exports.createMessageConnection = createMessageConnection2;
3454
3454
  var ril_1 = __importDefault(require_ril());
3455
3455
  ril_1.default.install();
3456
- var path8 = __importStar(__require("path"));
3456
+ var path9 = __importStar(__require("path"));
3457
3457
  var os2 = __importStar(__require("os"));
3458
3458
  var fs8 = __importStar(__require("fs"));
3459
3459
  var crypto_1 = __require("crypto");
@@ -3462,9 +3462,9 @@ var require_main = __commonJS({
3462
3462
  __exportStar(require_api(), exports);
3463
3463
  var IPCMessageReader = class extends api_1.AbstractMessageReader {
3464
3464
  process;
3465
- constructor(process10) {
3465
+ constructor(process12) {
3466
3466
  super();
3467
- this.process = process10;
3467
+ this.process = process12;
3468
3468
  const eventEmitter = this.process;
3469
3469
  eventEmitter.on("error", (error51) => this.fireError(error51));
3470
3470
  eventEmitter.on("close", () => this.fireClose());
@@ -3478,9 +3478,9 @@ var require_main = __commonJS({
3478
3478
  var IPCMessageWriter = class extends api_1.AbstractMessageWriter {
3479
3479
  process;
3480
3480
  errorCount;
3481
- constructor(process10) {
3481
+ constructor(process12) {
3482
3482
  super();
3483
- this.process = process10;
3483
+ this.process = process12;
3484
3484
  this.errorCount = 0;
3485
3485
  const eventEmitter = this.process;
3486
3486
  eventEmitter.on("error", (error51) => this.fireError(error51));
@@ -3605,7 +3605,7 @@ var require_main = __commonJS({
3605
3605
  throw new Error(`Unable to generate a random pipe name with ${randomLength} characters.`);
3606
3606
  }
3607
3607
  const randomSuffix = (0, crypto_1.randomBytes)(Math.floor(randomLength / 2)).toString("hex");
3608
- return path8.join(tmpDir, `lsp-${randomSuffix}.sock`);
3608
+ return path9.join(tmpDir, `lsp-${randomSuffix}.sock`);
3609
3609
  }
3610
3610
  function createClientPipeTransport(pipeName, encoding = "utf-8") {
3611
3611
  let connectResolve;
@@ -4509,10 +4509,10 @@ function mergeDefs(...defs) {
4509
4509
  function cloneDef(schema) {
4510
4510
  return mergeDefs(schema._zod.def);
4511
4511
  }
4512
- function getElementAtPath(obj, path8) {
4513
- if (!path8)
4512
+ function getElementAtPath(obj, path9) {
4513
+ if (!path9)
4514
4514
  return obj;
4515
- return path8.reduce((acc, key) => acc?.[key], obj);
4515
+ return path9.reduce((acc, key) => acc?.[key], obj);
4516
4516
  }
4517
4517
  function promiseAllObject(promisesObj) {
4518
4518
  const keys = Object.keys(promisesObj);
@@ -4921,11 +4921,11 @@ function explicitlyAborted(x, startIndex = 0) {
4921
4921
  }
4922
4922
  return false;
4923
4923
  }
4924
- function prefixIssues(path8, issues) {
4924
+ function prefixIssues(path9, issues) {
4925
4925
  return issues.map((iss) => {
4926
4926
  var _a3;
4927
4927
  (_a3 = iss).path ?? (_a3.path = []);
4928
- iss.path.unshift(path8);
4928
+ iss.path.unshift(path9);
4929
4929
  return iss;
4930
4930
  });
4931
4931
  }
@@ -5072,16 +5072,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
5072
5072
  }
5073
5073
  function formatError(error51, mapper = (issue2) => issue2.message) {
5074
5074
  const fieldErrors = { _errors: [] };
5075
- const processError = (error52, path8 = []) => {
5075
+ const processError = (error52, path9 = []) => {
5076
5076
  for (const issue2 of error52.issues) {
5077
5077
  if (issue2.code === "invalid_union" && issue2.errors.length) {
5078
- issue2.errors.map((issues) => processError({ issues }, [...path8, ...issue2.path]));
5078
+ issue2.errors.map((issues) => processError({ issues }, [...path9, ...issue2.path]));
5079
5079
  } else if (issue2.code === "invalid_key") {
5080
- processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
5080
+ processError({ issues: issue2.issues }, [...path9, ...issue2.path]);
5081
5081
  } else if (issue2.code === "invalid_element") {
5082
- processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
5082
+ processError({ issues: issue2.issues }, [...path9, ...issue2.path]);
5083
5083
  } else {
5084
- const fullpath = [...path8, ...issue2.path];
5084
+ const fullpath = [...path9, ...issue2.path];
5085
5085
  if (fullpath.length === 0) {
5086
5086
  fieldErrors._errors.push(mapper(issue2));
5087
5087
  } else {
@@ -5108,17 +5108,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
5108
5108
  }
5109
5109
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
5110
5110
  const result = { errors: [] };
5111
- const processError = (error52, path8 = []) => {
5111
+ const processError = (error52, path9 = []) => {
5112
5112
  var _a3, _b;
5113
5113
  for (const issue2 of error52.issues) {
5114
5114
  if (issue2.code === "invalid_union" && issue2.errors.length) {
5115
- issue2.errors.map((issues) => processError({ issues }, [...path8, ...issue2.path]));
5115
+ issue2.errors.map((issues) => processError({ issues }, [...path9, ...issue2.path]));
5116
5116
  } else if (issue2.code === "invalid_key") {
5117
- processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
5117
+ processError({ issues: issue2.issues }, [...path9, ...issue2.path]);
5118
5118
  } else if (issue2.code === "invalid_element") {
5119
- processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
5119
+ processError({ issues: issue2.issues }, [...path9, ...issue2.path]);
5120
5120
  } else {
5121
- const fullpath = [...path8, ...issue2.path];
5121
+ const fullpath = [...path9, ...issue2.path];
5122
5122
  if (fullpath.length === 0) {
5123
5123
  result.errors.push(mapper(issue2));
5124
5124
  continue;
@@ -5150,8 +5150,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
5150
5150
  }
5151
5151
  function toDotPath(_path) {
5152
5152
  const segs = [];
5153
- const path8 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5154
- for (const seg of path8) {
5153
+ const path9 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5154
+ for (const seg of path9) {
5155
5155
  if (typeof seg === "number")
5156
5156
  segs.push(`[${seg}]`);
5157
5157
  else if (typeof seg === "symbol")
@@ -17843,13 +17843,13 @@ function resolveRef(ref, ctx) {
17843
17843
  if (!ref.startsWith("#")) {
17844
17844
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
17845
17845
  }
17846
- const path8 = ref.slice(1).split("/").filter(Boolean);
17847
- if (path8.length === 0) {
17846
+ const path9 = ref.slice(1).split("/").filter(Boolean);
17847
+ if (path9.length === 0) {
17848
17848
  return ctx.rootSchema;
17849
17849
  }
17850
17850
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
17851
- if (path8[0] === defsKey) {
17852
- const key = path8[1];
17851
+ if (path9[0] === defsKey) {
17852
+ const key = path9[1];
17853
17853
  if (!key || !ctx.defs[key]) {
17854
17854
  throw new Error(`Reference not found: ${ref}`);
17855
17855
  }
@@ -18856,21 +18856,6 @@ var zAgentCapabilities = object({
18856
18856
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => void 0)
18857
18857
  });
18858
18858
  var zAuthMethodId = string2();
18859
- var zAuthEnvVar = object({
18860
- name: string2(),
18861
- label: defaultOnError(string2().nullish(), () => void 0),
18862
- secret: defaultOnError(boolean2().optional().default(true), () => true),
18863
- optional: defaultOnError(boolean2().optional().default(false), () => false),
18864
- _meta: defaultOnError(record(string2(), unknown()).nullish(), () => void 0)
18865
- });
18866
- var zAuthMethodEnvVar = object({
18867
- id: zAuthMethodId,
18868
- name: string2(),
18869
- description: defaultOnError(string2().nullish(), () => void 0),
18870
- vars: requiredDefaultOnError(vecSkipError(zAuthEnvVar), () => []),
18871
- link: defaultOnError(string2().nullish(), () => void 0),
18872
- _meta: defaultOnError(record(string2(), unknown()).nullish(), () => void 0)
18873
- });
18874
18859
  var zAuthMethodTerminal = object({
18875
18860
  id: zAuthMethodId,
18876
18861
  name: string2(),
@@ -18886,9 +18871,6 @@ var zAuthMethodAgent = object({
18886
18871
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => void 0)
18887
18872
  });
18888
18873
  var zAuthMethod = union([
18889
- zAuthMethodEnvVar.and(object({
18890
- type: literal("env_var")
18891
- })),
18892
18874
  zAuthMethodTerminal.and(object({
18893
18875
  type: literal("terminal")
18894
18876
  })),
@@ -19339,6 +19321,26 @@ var zUsageUpdate = object({
19339
19321
  cost: defaultOnError(zCost.nullish(), () => void 0),
19340
19322
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => void 0)
19341
19323
  });
19324
+ var zCompactionId = string2();
19325
+ var zCompactionStatus = union([
19326
+ literal("in_progress"),
19327
+ literal("completed"),
19328
+ literal("failed"),
19329
+ literal("cancelled"),
19330
+ string2()
19331
+ ]);
19332
+ var zCompactionUpdate = object({
19333
+ compactionId: zCompactionId,
19334
+ status: zCompactionStatus,
19335
+ summary: defaultOnError(vecSkipError(zContentBlock).nullish(), () => void 0),
19336
+ error: defaultOnError(string2().nullish(), () => void 0),
19337
+ _meta: defaultOnError(record(string2(), unknown()).nullish(), () => void 0)
19338
+ });
19339
+ var zCompactionSummaryChunk = object({
19340
+ compactionId: zCompactionId,
19341
+ content: zContentBlock,
19342
+ _meta: defaultOnError(record(string2(), unknown()).nullish(), () => void 0)
19343
+ });
19342
19344
  var zSessionUpdate = union([
19343
19345
  zContentChunk.and(object({
19344
19346
  sessionUpdate: literal("user_message_chunk")
@@ -19378,6 +19380,12 @@ var zSessionUpdate = union([
19378
19380
  })),
19379
19381
  zUsageUpdate.and(object({
19380
19382
  sessionUpdate: literal("usage_update")
19383
+ })),
19384
+ zCompactionUpdate.and(object({
19385
+ sessionUpdate: literal("compaction_update")
19386
+ })),
19387
+ zCompactionSummaryChunk.and(object({
19388
+ sessionUpdate: literal("compaction_summary_chunk")
19381
19389
  }))
19382
19390
  ]);
19383
19391
  var zSessionNotification = object({
@@ -19410,6 +19418,7 @@ var zFileSystemCapabilities = object({
19410
19418
  writeTextFile: defaultOnError(boolean2().optional().default(false), () => false),
19411
19419
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => void 0)
19412
19420
  });
19421
+ var zCompactionCapabilities = record(string2(), unknown());
19413
19422
  var zBooleanConfigOptionCapabilities = object({
19414
19423
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => void 0)
19415
19424
  });
@@ -19418,6 +19427,7 @@ var zSessionConfigOptionsCapabilities = object({
19418
19427
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => void 0)
19419
19428
  });
19420
19429
  var zClientSessionCapabilities = object({
19430
+ compaction: defaultOnError(zCompactionCapabilities.nullish(), () => void 0),
19421
19431
  configOptions: defaultOnError(zSessionConfigOptionsCapabilities.nullish(), () => void 0),
19422
19432
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => void 0)
19423
19433
  });
@@ -20096,6 +20106,7 @@ var RequestResponder = class {
20096
20106
  });
20097
20107
  }
20098
20108
  };
20109
+ var requestBatchSizes = /* @__PURE__ */ new WeakMap();
20099
20110
  var HandlerRegistration = class {
20100
20111
  disposeHandler;
20101
20112
  active = true;
@@ -20446,9 +20457,6 @@ var Connection = class {
20446
20457
  if (done) {
20447
20458
  break;
20448
20459
  }
20449
- if (!message) {
20450
- continue;
20451
- }
20452
20460
  this.receiveWireMessage(message);
20453
20461
  }
20454
20462
  } finally {
@@ -20472,19 +20480,16 @@ var Connection = class {
20472
20480
  this.receiveBatch(message);
20473
20481
  return;
20474
20482
  }
20475
- if (!isRecord(message)) {
20476
- console.error("Invalid message", { message });
20483
+ if (!isRequestMessage(message) && !isNotificationMessage(message) && !isResponseShapedMessage(message)) {
20484
+ void this.sendWireMessage(protocolErrorResponse(RequestError.invalidRequest(message))).catch(() => {
20485
+ });
20477
20486
  return;
20478
20487
  }
20479
20488
  this.receiveMessage(message);
20480
20489
  }
20481
20490
  receiveBatch(batch) {
20482
20491
  if (batch.length === 0) {
20483
- void this.sendWireMessage({
20484
- jsonrpc: "2.0",
20485
- id: null,
20486
- error: RequestError.invalidRequest(batch).toErrorResponse()
20487
- }).catch(() => {
20492
+ void this.sendWireMessage(protocolErrorResponse(RequestError.invalidRequest(batch))).catch(() => {
20488
20493
  });
20489
20494
  return;
20490
20495
  }
@@ -20514,15 +20519,11 @@ var Connection = class {
20514
20519
  continue;
20515
20520
  }
20516
20521
  if (!isRequestMessage(message) && !isNotificationMessage(message)) {
20517
- void collectResponse({
20518
- jsonrpc: "2.0",
20519
- id: null,
20520
- error: RequestError.invalidRequest(message).toErrorResponse()
20521
- }).catch(() => {
20522
+ void collectResponse(protocolErrorResponse(RequestError.invalidRequest(message))).catch(() => {
20522
20523
  });
20523
20524
  continue;
20524
20525
  }
20525
- const processing = this.receiveMessage(message, isRequestMessage(message) ? collectResponse : void 0);
20526
+ const processing = this.receiveMessage(message, isRequestMessage(message) ? collectResponse : void 0, batch.length);
20526
20527
  if (isNotificationMessage(message)) {
20527
20528
  void processing.finally(() => {
20528
20529
  remainingNotifications -= 1;
@@ -20531,7 +20532,7 @@ var Connection = class {
20531
20532
  }
20532
20533
  }
20533
20534
  }
20534
- receiveMessage(message, sendResponse) {
20535
+ receiveMessage(message, sendResponse, batchSize) {
20535
20536
  if (this.abortController.signal.aborted) {
20536
20537
  return Promise.resolve();
20537
20538
  }
@@ -20543,7 +20544,7 @@ var Connection = class {
20543
20544
  if (!("id" in message)) {
20544
20545
  this.handleProtocolNotification(message);
20545
20546
  }
20546
- return this.processIncomingMessage(this.toIncomingMessage(message, sendResponse)).catch((error51) => this.close(error51));
20547
+ return this.processIncomingMessage(this.toIncomingMessage(message, sendResponse, batchSize)).catch((error51) => this.close(error51));
20547
20548
  } else if ("id" in message) {
20548
20549
  this.handleResponse(message);
20549
20550
  } else {
@@ -20593,7 +20594,7 @@ var Connection = class {
20593
20594
  }
20594
20595
  }
20595
20596
  }
20596
- toIncomingMessage(message, sendResponse) {
20597
+ toIncomingMessage(message, sendResponse, batchSize) {
20597
20598
  if ("id" in message) {
20598
20599
  const abortController = new AbortController();
20599
20600
  this.incomingRequests.set(message.id, abortController);
@@ -20602,20 +20603,24 @@ var Connection = class {
20602
20603
  this.incomingRequests.delete(message.id);
20603
20604
  }
20604
20605
  };
20606
+ const responder = new RequestResponder(message.id, (result) => {
20607
+ const response = {
20608
+ jsonrpc: "2.0",
20609
+ id: message.id,
20610
+ ...result
20611
+ };
20612
+ return sendResponse ? sendResponse(response) : this.sendWireMessage(response);
20613
+ }, abortController.signal, finishRequest);
20614
+ if (batchSize !== void 0) {
20615
+ requestBatchSizes.set(responder, batchSize);
20616
+ }
20605
20617
  return {
20606
20618
  kind: "request",
20607
20619
  method: message.method,
20608
20620
  params: message.params,
20609
20621
  raw: message,
20610
20622
  signal: abortController.signal,
20611
- responder: new RequestResponder(message.id, (result) => {
20612
- const response = {
20613
- jsonrpc: "2.0",
20614
- id: message.id,
20615
- ...result
20616
- };
20617
- return sendResponse ? sendResponse(response) : this.sendWireMessage(response);
20618
- }, abortController.signal, finishRequest)
20623
+ responder
20619
20624
  };
20620
20625
  }
20621
20626
  return {
@@ -20837,6 +20842,13 @@ var RequestError = class _RequestError extends Error {
20837
20842
  };
20838
20843
  }
20839
20844
  };
20845
+ function protocolErrorResponse(error51) {
20846
+ return {
20847
+ jsonrpc: "2.0",
20848
+ id: null,
20849
+ error: error51.toErrorResponse()
20850
+ };
20851
+ }
20840
20852
 
20841
20853
  // node_modules/@agentclientprotocol/sdk/dist/line-buffer.js
20842
20854
  var newline = 10;
@@ -20897,22 +20909,40 @@ function ndJsonStream(output, input) {
20897
20909
  const textDecoder = new TextDecoder();
20898
20910
  let cancelled = false;
20899
20911
  let inputReader;
20912
+ let outputWrite = Promise.resolve();
20913
+ const writeJson = (message) => {
20914
+ const content = JSON.stringify(message) + "\n";
20915
+ const write = outputWrite.then(async () => {
20916
+ const writer = output.getWriter();
20917
+ try {
20918
+ await writer.write(textEncoder.encode(content));
20919
+ } finally {
20920
+ writer.releaseLock();
20921
+ }
20922
+ });
20923
+ outputWrite = write.catch(() => {
20924
+ });
20925
+ return write;
20926
+ };
20900
20927
  const readable = new ReadableStream({
20901
20928
  async start(controller) {
20902
20929
  const lines = new LineBuffer();
20903
- const enqueueLine = (lineBytes) => {
20930
+ const enqueueLine = async (lineBytes) => {
20904
20931
  const trimmedLine = textDecoder.decode(lineBytes).trim();
20905
- if (trimmedLine) {
20906
- try {
20907
- const message = JSON.parse(trimmedLine);
20908
- if (isRecord(message) || Array.isArray(message)) {
20909
- controller.enqueue(message);
20910
- } else {
20911
- console.warn("Skipping JSON line that is not an object:", trimmedLine);
20912
- }
20913
- } catch (err) {
20914
- console.error("Failed to parse JSON message:", trimmedLine, err);
20915
- }
20932
+ if (!trimmedLine) {
20933
+ return;
20934
+ }
20935
+ let message;
20936
+ try {
20937
+ message = JSON.parse(trimmedLine);
20938
+ } catch {
20939
+ await writeJson(protocolErrorResponse(RequestError.parseError()));
20940
+ return;
20941
+ }
20942
+ if (isRecord(message) || Array.isArray(message)) {
20943
+ controller.enqueue(message);
20944
+ } else {
20945
+ await writeJson(protocolErrorResponse(RequestError.invalidRequest(message)));
20916
20946
  }
20917
20947
  };
20918
20948
  const reader = input.getReader();
@@ -20930,7 +20960,7 @@ function ndJsonStream(output, input) {
20930
20960
  continue;
20931
20961
  }
20932
20962
  for (const line of lines.push(value)) {
20933
- enqueueLine(line);
20963
+ await enqueueLine(line);
20934
20964
  if (cancelled) {
20935
20965
  return;
20936
20966
  }
@@ -20941,7 +20971,7 @@ function ndJsonStream(output, input) {
20941
20971
  }
20942
20972
  const lastLine = lines.flush();
20943
20973
  if (lastLine) {
20944
- enqueueLine(lastLine);
20974
+ await enqueueLine(lastLine);
20945
20975
  }
20946
20976
  } catch (err) {
20947
20977
  if (cancelled) {
@@ -20966,14 +20996,8 @@ function ndJsonStream(output, input) {
20966
20996
  }
20967
20997
  });
20968
20998
  const writable = new WritableStream({
20969
- async write(message) {
20970
- const content = JSON.stringify(message) + "\n";
20971
- const writer = output.getWriter();
20972
- try {
20973
- await writer.write(textEncoder.encode(content));
20974
- } finally {
20975
- writer.releaseLock();
20976
- }
20999
+ write(message) {
21000
+ return writeJson(message);
20977
21001
  }
20978
21002
  });
20979
21003
  return { readable, writable };
@@ -21592,11 +21616,11 @@ var clientRequestSpecs = {
21592
21616
  releaseTerminal: requestSpec(CLIENT_METHODS.terminal_release, zReleaseTerminalRequest, emptyObjectResponse),
21593
21617
  waitForTerminalExit: requestSpec(CLIENT_METHODS.terminal_wait_for_exit, zWaitForTerminalExitRequest),
21594
21618
  killTerminal: requestSpec(CLIENT_METHODS.terminal_kill, zKillTerminalRequest, emptyObjectResponse),
21595
- unstable_createElicitation: requestSpec(CLIENT_METHODS.elicitation_create, zCreateElicitationRequest)
21619
+ createElicitation: requestSpec(CLIENT_METHODS.elicitation_create, zCreateElicitationRequest)
21596
21620
  };
21597
21621
  var clientNotificationSpecs = {
21598
21622
  sessionUpdate: notificationSpec(CLIENT_METHODS.session_update, zSessionNotification),
21599
- unstable_completeElicitation: notificationSpec(CLIENT_METHODS.elicitation_complete, zCompleteElicitationNotification)
21623
+ completeElicitation: notificationSpec(CLIENT_METHODS.elicitation_complete, zCompleteElicitationNotification)
21600
21624
  };
21601
21625
  var agentRequestSpecsByMethod = specsByMethod(agentRequestSpecs);
21602
21626
  var agentNotificationSpecsByMethod = specsByMethod(agentNotificationSpecs);
@@ -22021,7 +22045,12 @@ var Logger = class {
22021
22045
  }
22022
22046
  }
22023
22047
  error(message, err) {
22024
- this.log(`[SYSTEM_ERROR] ${message}`, { exception: this.formatError(err) });
22048
+ const formattedError = this.formatError(err);
22049
+ if (!this.logFilePath) {
22050
+ console.error(`[SYSTEM_ERROR] ${message}: ${formattedError}`);
22051
+ return;
22052
+ }
22053
+ this.log(`[SYSTEM_ERROR] ${message}`, { exception: formattedError });
22025
22054
  }
22026
22055
  log(message, context) {
22027
22056
  if (!this.logFilePath) return;
@@ -22102,6 +22131,36 @@ function attachLogs(proc) {
22102
22131
  });
22103
22132
  }
22104
22133
 
22134
+ // src/AirExtension.ts
22135
+ var JETBRAINS_META_KEY = "jetbrains";
22136
+ var AIR_META_KEY = "air";
22137
+ var AIR_EXTENSION_VERSION_KEY = "version";
22138
+ var AIR_EXTENSION_CAPABILITIES_KEY = "capabilities";
22139
+ var AIR_SESSION_FAILURE_KEY = "sessionFailure";
22140
+ var AIR_AGENT_FILE_CHANGE_REPORT_KEY = "agentFileChangeReport";
22141
+ var AIR_NATIVE_SUBAGENT_SESSIONS_KEY = "nativeSubagentSessions";
22142
+ var AIR_AGENT_FILE_CHANGE_REPORT_REQUEST_KEY = "agentFileChangeReportRequest";
22143
+ var AIR_EXTENSION_VERSION = 1;
22144
+ function clientSupportsAirCapability(capabilities, capability) {
22145
+ const jetbrains = capabilities?._meta?.[JETBRAINS_META_KEY];
22146
+ const air = jetbrains?.[AIR_META_KEY];
22147
+ const version2 = air?.[AIR_EXTENSION_VERSION_KEY];
22148
+ const supported = air?.[AIR_EXTENSION_CAPABILITIES_KEY];
22149
+ return typeof version2 === "number" && Number.isInteger(version2) && version2 >= AIR_EXTENSION_VERSION && Array.isArray(supported) && supported.includes(capability);
22150
+ }
22151
+
22152
+ // src/subagents/AcpSubagents.ts
22153
+ function clientSupportsSubagents(capabilities) {
22154
+ const subagents = capabilities?.subagents;
22155
+ if (typeof subagents === "object" && subagents !== null && !Array.isArray(subagents)) {
22156
+ return true;
22157
+ }
22158
+ return clientSupportsAirCapability(capabilities, AIR_NATIVE_SUBAGENT_SESSIONS_KEY);
22159
+ }
22160
+ function asSdkSessionNotification(notification) {
22161
+ return notification;
22162
+ }
22163
+
22105
22164
  // src/ACPSessionConnection.ts
22106
22165
  var ACPSessionConnection = class {
22107
22166
  connection;
@@ -22110,11 +22169,11 @@ var ACPSessionConnection = class {
22110
22169
  this.connection = connection;
22111
22170
  this.sessionId = sessionId;
22112
22171
  }
22113
- async update(update) {
22114
- await this.connection.notify(methods.client.session.update, {
22115
- sessionId: this.sessionId,
22172
+ async update(update, sessionId = this.sessionId) {
22173
+ await this.connection.notify(methods.client.session.update, asSdkSessionNotification({
22174
+ sessionId,
22116
22175
  update
22117
- });
22176
+ }));
22118
22177
  }
22119
22178
  };
22120
22179
 
@@ -23248,13 +23307,13 @@ function createTerminalCommandEvent(event, terminalId, cwd) {
23248
23307
  }
23249
23308
  };
23250
23309
  }
23251
- function createSearchTitle(query, path8) {
23252
- if (query && path8) {
23253
- return `Search for '${query}' in ${path8}`;
23310
+ function createSearchTitle(query, path9) {
23311
+ if (query && path9) {
23312
+ return `Search for '${query}' in ${path9}`;
23254
23313
  } else if (query) {
23255
23314
  return `Search for '${query}'`;
23256
- } else if (path8) {
23257
- return `Search in '${path8}'`;
23315
+ } else if (path9) {
23316
+ return `Search in '${path9}'`;
23258
23317
  }
23259
23318
  return "Search";
23260
23319
  }
@@ -23480,12 +23539,12 @@ function revertPatch(unifiedDiff) {
23480
23539
  if (!patch) return null;
23481
23540
  return reversePatch(patch);
23482
23541
  }
23483
- function createUpdateDiffContent(path8, oldText, newText) {
23542
+ function createUpdateDiffContent(path9, oldText, newText) {
23484
23543
  return {
23485
23544
  type: "diff",
23486
23545
  oldText,
23487
23546
  newText,
23488
- path: path8,
23547
+ path: path9,
23489
23548
  _meta: {
23490
23549
  kind: "update"
23491
23550
  }
@@ -23612,15 +23671,380 @@ function sameThreadGoalSnapshot(left, right) {
23612
23671
  // src/CodexEventHandler.ts
23613
23672
  import { randomUUID } from "node:crypto";
23614
23673
 
23615
- // src/AirExtension.ts
23616
- var JETBRAINS_META_KEY = "jetbrains";
23617
- var AIR_META_KEY = "air";
23618
- var AIR_EXTENSION_VERSION_KEY = "version";
23619
- var AIR_EXTENSION_CAPABILITIES_KEY = "capabilities";
23620
- var AIR_SESSION_FAILURE_KEY = "sessionFailure";
23621
- var AIR_AGENT_FILE_CHANGE_REPORT_KEY = "agentFileChangeReport";
23622
- var AIR_AGENT_FILE_CHANGE_REPORT_REQUEST_KEY = "agentFileChangeReportRequest";
23623
- var AIR_EXTENSION_VERSION = 1;
23674
+ // src/subagents/CodexAgentPath.ts
23675
+ function normalizeAgentPath(path9) {
23676
+ const normalized = path9.trim().replace(/\/+$/, "");
23677
+ return normalized || "/root";
23678
+ }
23679
+ function isRootAgentPath(path9) {
23680
+ const normalized = normalizeAgentPath(path9);
23681
+ return normalized === "/root" || normalized === "root";
23682
+ }
23683
+ function nameFromAgentPath(path9, fallback) {
23684
+ const normalized = normalizeAgentPath(path9);
23685
+ const name = normalized.slice(normalized.lastIndexOf("/") + 1).trim();
23686
+ if (!name) return fallback;
23687
+ const words = name.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
23688
+ return words ? words.charAt(0).toUpperCase() + words.slice(1) : fallback;
23689
+ }
23690
+
23691
+ // src/subagents/CodexSubagentEventRouter.ts
23692
+ var CodexSubagentEventRouter = class _CodexSubagentEventRouter {
23693
+ constructor(rootSessionId, supported, session) {
23694
+ this.rootSessionId = rootSessionId;
23695
+ this.supported = supported;
23696
+ this.session = session;
23697
+ }
23698
+ rootSessionId;
23699
+ supported;
23700
+ session;
23701
+ static DEFAULT_WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
23702
+ static MAX_PENDING_NOTIFICATIONS = 256;
23703
+ children = /* @__PURE__ */ new Map();
23704
+ pendingSpawns = /* @__PURE__ */ new Map();
23705
+ terminalPendingSpawns = /* @__PURE__ */ new Map();
23706
+ waiters = /* @__PURE__ */ new Set();
23707
+ materializationWaiters = /* @__PURE__ */ new Map();
23708
+ replayQueue = [];
23709
+ activeLegacyActivities = /* @__PURE__ */ new Set();
23710
+ async handle(notification) {
23711
+ if (notification.method === "turn/started") {
23712
+ return this.isKnownChild(notification.params.threadId);
23713
+ }
23714
+ if (notification.method === "turn/completed") {
23715
+ const childTurn = this.isKnownChild(notification.params.threadId);
23716
+ const state = terminalStateFromTurn(notification.params.turn.status);
23717
+ if (!state) return childTurn;
23718
+ if (notification.params.threadId === this.rootSessionId) {
23719
+ if (state !== "completed") await this.finishOutstanding(state);
23720
+ } else {
23721
+ if (this.pendingSpawns.has(notification.params.threadId)) {
23722
+ this.finishPending(notification.params.threadId);
23723
+ } else {
23724
+ await this.finish(notification.params.threadId, state);
23725
+ }
23726
+ }
23727
+ return childTurn;
23728
+ }
23729
+ const notificationThreadId = notification.params.threadId;
23730
+ if (typeof notificationThreadId === "string" && this.pendingSpawns.has(notificationThreadId)) {
23731
+ const pending = this.pendingSpawns.get(notificationThreadId);
23732
+ if (pending.buffered.length === _CodexSubagentEventRouter.MAX_PENDING_NOTIFICATIONS) {
23733
+ pending.buffered.shift();
23734
+ pending.droppedBufferedNotifications += 1;
23735
+ if (pending.droppedBufferedNotifications === 1) {
23736
+ logger.log(`Pending subagent ${notificationThreadId} exceeded the notification buffer; dropping oldest updates`);
23737
+ }
23738
+ }
23739
+ pending.buffered.push(notification);
23740
+ return true;
23741
+ }
23742
+ if (notification.method !== "item/started" && notification.method !== "item/completed") {
23743
+ return false;
23744
+ }
23745
+ const item = notification.params.item;
23746
+ if (!this.supported) {
23747
+ return false;
23748
+ }
23749
+ if (item.type === "subAgentActivity") {
23750
+ if (isRootAgentPath(item.agentPath)) return true;
23751
+ if (this.terminalPendingSpawns.has(item.agentThreadId)) return true;
23752
+ let hasNativeRepresentation = this.children.has(item.agentThreadId);
23753
+ if (!hasNativeRepresentation) {
23754
+ await this.materialize(item.agentThreadId, item.agentPath);
23755
+ hasNativeRepresentation = this.children.has(item.agentThreadId);
23756
+ }
23757
+ if (hasNativeRepresentation && item.kind === "interrupted") {
23758
+ await this.finish(item.agentThreadId, "cancelled");
23759
+ }
23760
+ return hasNativeRepresentation;
23761
+ }
23762
+ if (item.type !== "collabAgentToolCall") return false;
23763
+ if (item.tool === "resumeAgent" || item.tool === "sendInput") {
23764
+ for (const [childThreadId, state] of Object.entries(item.agentsStates)) {
23765
+ if (state?.status === "running" || state?.status === "pendingInit") {
23766
+ await this.reopen(childThreadId);
23767
+ }
23768
+ }
23769
+ }
23770
+ let representedSpawn = false;
23771
+ if (item.tool === "spawnAgent") {
23772
+ const parent = this.children.get(item.senderThreadId);
23773
+ const parentThreadId = parent ? item.senderThreadId : this.rootSessionId;
23774
+ const parentSessionId = parent?.sessionId ?? this.rootSessionId;
23775
+ for (const childSessionId of item.receiverThreadIds) {
23776
+ if (childSessionId.trim().length === 0) {
23777
+ logger.log("Ignoring spawned subagent with an empty thread id");
23778
+ continue;
23779
+ }
23780
+ if (childSessionId === parentSessionId || childSessionId === this.rootSessionId) {
23781
+ logger.log(`Ignoring self-referential spawned subagent ${childSessionId}`);
23782
+ continue;
23783
+ }
23784
+ if (this.children.has(childSessionId) || this.pendingSpawns.has(childSessionId) || this.terminalPendingSpawns.has(childSessionId)) {
23785
+ representedSpawn = true;
23786
+ continue;
23787
+ }
23788
+ this.pendingSpawns.set(childSessionId, {
23789
+ parentThreadId,
23790
+ parentSessionId,
23791
+ task: item.prompt?.trim() || "Delegated task",
23792
+ buffered: [],
23793
+ droppedBufferedNotifications: 0
23794
+ });
23795
+ representedSpawn = true;
23796
+ }
23797
+ }
23798
+ for (const [childSessionId, state] of Object.entries(item.agentsStates)) {
23799
+ const terminalState = state && terminalStateOf(state.status);
23800
+ if (!terminalState) continue;
23801
+ if (this.children.has(childSessionId)) await this.finish(childSessionId, terminalState);
23802
+ else if (this.pendingSpawns.has(childSessionId)) this.finishPending(childSessionId);
23803
+ }
23804
+ if (item.tool === "spawnAgent" && item.status === "failed") {
23805
+ for (const childSessionId of item.receiverThreadIds) {
23806
+ if (this.pendingSpawns.has(childSessionId)) this.finishPending(childSessionId);
23807
+ }
23808
+ }
23809
+ return item.tool === "spawnAgent" && representedSpawn;
23810
+ }
23811
+ shouldIgnore(notification) {
23812
+ const threadId = notification.params.threadId;
23813
+ const ignored = typeof threadId === "string" && (this.children.get(threadId)?.terminalState !== void 0 || this.terminalPendingSpawns.has(threadId));
23814
+ if (ignored) logger.log(`Ignoring update for terminal subagent ${threadId}`);
23815
+ return ignored;
23816
+ }
23817
+ notificationSessionId(notification) {
23818
+ const threadId = notification.params.threadId;
23819
+ return typeof threadId === "string" && this.children.has(threadId) ? this.children.get(threadId).sessionId : this.rootSessionId;
23820
+ }
23821
+ takeBufferedNotifications() {
23822
+ return this.replayQueue.splice(0);
23823
+ }
23824
+ async waitForMaterializedSession(childThreadId) {
23825
+ const child = this.children.get(childThreadId);
23826
+ if (child) return child.terminalState === void 0 ? child.sessionId : null;
23827
+ if (this.terminalPendingSpawns.has(childThreadId)) return null;
23828
+ if (!this.pendingSpawns.has(childThreadId)) return null;
23829
+ return await new Promise((resolve) => {
23830
+ const waiters = this.materializationWaiters.get(childThreadId) ?? /* @__PURE__ */ new Set();
23831
+ waiters.add(resolve);
23832
+ this.materializationWaiters.set(childThreadId, waiters);
23833
+ });
23834
+ }
23835
+ legacyActivityStarted(item) {
23836
+ this.activeLegacyActivities.add(item.id);
23837
+ return createSubAgentActivityUpdate(item, "in_progress", "tool_call");
23838
+ }
23839
+ legacyCollaborationStarted(item) {
23840
+ return createCollabAgentToolCallUpdate(item);
23841
+ }
23842
+ legacyCollaborationCompleted(item) {
23843
+ return createCollabAgentToolCallCompleteUpdate(item);
23844
+ }
23845
+ legacyActivityCompleted(item) {
23846
+ const sessionUpdate = this.activeLegacyActivities.delete(item.id) ? "tool_call_update" : "tool_call";
23847
+ return createSubAgentActivityUpdate(item, "completed", sessionUpdate);
23848
+ }
23849
+ async wait(signal, timeoutMs = _CodexSubagentEventRouter.DEFAULT_WAIT_TIMEOUT_MS) {
23850
+ const deadline = Date.now() + timeoutMs;
23851
+ while (this.hasOutstanding()) {
23852
+ if (signal.aborted) return;
23853
+ const remainingMs = deadline - Date.now();
23854
+ if (remainingMs <= 0) {
23855
+ logger.log(`Timed out waiting for subagents in session ${this.rootSessionId}; marking them failed`);
23856
+ await this.finishOutstanding("failed");
23857
+ return;
23858
+ }
23859
+ const changed = await new Promise((resolve) => {
23860
+ const timeout = setTimeout(() => {
23861
+ this.waiters.delete(onChange);
23862
+ signal.removeEventListener("abort", onAbort);
23863
+ resolve(false);
23864
+ }, remainingMs);
23865
+ const onAbort = () => {
23866
+ clearTimeout(timeout);
23867
+ this.waiters.delete(onChange);
23868
+ resolve(true);
23869
+ };
23870
+ const onChange = () => {
23871
+ clearTimeout(timeout);
23872
+ signal.removeEventListener("abort", onAbort);
23873
+ resolve(true);
23874
+ };
23875
+ this.waiters.add(onChange);
23876
+ signal.addEventListener("abort", onAbort, { once: true });
23877
+ });
23878
+ if (!changed) {
23879
+ logger.log(`Timed out waiting for subagents in session ${this.rootSessionId}; marking them failed`);
23880
+ await this.finishOutstanding("failed");
23881
+ return;
23882
+ }
23883
+ }
23884
+ }
23885
+ async finishOutstanding(state) {
23886
+ for (const childSessionId of [...this.pendingSpawns.keys()]) {
23887
+ this.finishPending(childSessionId);
23888
+ }
23889
+ for (const childSessionId of [...this.children.keys()].reverse()) {
23890
+ await this.finish(childSessionId, state);
23891
+ }
23892
+ }
23893
+ isKnownChild(threadId) {
23894
+ return threadId !== this.rootSessionId && (this.children.has(threadId) || this.pendingSpawns.has(threadId) || this.terminalPendingSpawns.has(threadId));
23895
+ }
23896
+ async materialize(childSessionId, path9) {
23897
+ if (this.children.has(childSessionId)) return;
23898
+ const pending = this.pendingSpawns.get(childSessionId);
23899
+ const name = nameFromAgentPath(path9, fallbackName(childSessionId));
23900
+ const inferredParent = this.parentForPath(path9);
23901
+ const parentThreadId = pending?.parentThreadId ?? inferredParent.threadId;
23902
+ const parentSessionId = pending?.parentSessionId ?? inferredParent.sessionId;
23903
+ const task = pending?.task ?? `Delegated task for ${name}`;
23904
+ await this.session.update({
23905
+ sessionUpdate: "subagent_spawned",
23906
+ subagentSessionId: childSessionId,
23907
+ name,
23908
+ task,
23909
+ capabilities: {}
23910
+ }, parentSessionId);
23911
+ this.children.set(childSessionId, {
23912
+ parentThreadId,
23913
+ parentSessionId,
23914
+ sessionId: childSessionId,
23915
+ name,
23916
+ task,
23917
+ path: normalizeAgentPath(path9),
23918
+ generation: 1
23919
+ });
23920
+ this.pendingSpawns.delete(childSessionId);
23921
+ this.replayQueue.push(...pending?.buffered ?? []);
23922
+ this.resolveMaterialization(childSessionId, childSessionId);
23923
+ }
23924
+ finishPending(childSessionId) {
23925
+ const pending = this.pendingSpawns.get(childSessionId);
23926
+ if (!pending) return;
23927
+ this.pendingSpawns.delete(childSessionId);
23928
+ this.terminalPendingSpawns.set(childSessionId, pending);
23929
+ this.resolveMaterialization(childSessionId, null);
23930
+ this.notifyWaiters();
23931
+ }
23932
+ async finish(childSessionId, state) {
23933
+ const child = this.children.get(childSessionId);
23934
+ if (!child || child.terminalState !== void 0) return;
23935
+ child.terminalState = state;
23936
+ try {
23937
+ await this.session.update({
23938
+ sessionUpdate: "subagent_state_update",
23939
+ subagentSessionId: child.sessionId,
23940
+ state
23941
+ }, child.parentSessionId);
23942
+ } catch (error51) {
23943
+ if (child.terminalState === state) delete child.terminalState;
23944
+ throw error51;
23945
+ }
23946
+ this.notifyWaiters();
23947
+ }
23948
+ async reopen(childThreadId) {
23949
+ const child = this.children.get(childThreadId);
23950
+ if (!child) {
23951
+ const pending = this.terminalPendingSpawns.get(childThreadId);
23952
+ if (!pending) return;
23953
+ const parentSessionId = this.children.get(pending.parentThreadId)?.sessionId ?? this.rootSessionId;
23954
+ const reopened = {
23955
+ parentThreadId: pending.parentThreadId,
23956
+ parentSessionId,
23957
+ sessionId: `${childThreadId}:generation:2`,
23958
+ name: fallbackName(childThreadId),
23959
+ task: pending.task,
23960
+ generation: 2
23961
+ };
23962
+ await this.session.update({
23963
+ sessionUpdate: "subagent_spawned",
23964
+ subagentSessionId: reopened.sessionId,
23965
+ name: reopened.name,
23966
+ task: reopened.task,
23967
+ capabilities: {}
23968
+ }, parentSessionId);
23969
+ this.children.set(childThreadId, reopened);
23970
+ this.terminalPendingSpawns.delete(childThreadId);
23971
+ return;
23972
+ }
23973
+ if (!child.terminalState) return;
23974
+ const previousSessionId = child.sessionId;
23975
+ const previousParentSessionId = child.parentSessionId;
23976
+ const previousState = child.terminalState;
23977
+ child.parentSessionId = this.children.get(child.parentThreadId)?.sessionId ?? this.rootSessionId;
23978
+ child.generation += 1;
23979
+ child.sessionId = `${childThreadId}:generation:${child.generation}`;
23980
+ delete child.terminalState;
23981
+ try {
23982
+ await this.session.update({
23983
+ sessionUpdate: "subagent_spawned",
23984
+ subagentSessionId: child.sessionId,
23985
+ name: child.name,
23986
+ task: child.task,
23987
+ capabilities: {}
23988
+ }, child.parentSessionId);
23989
+ } catch (error51) {
23990
+ child.generation -= 1;
23991
+ child.sessionId = previousSessionId;
23992
+ child.parentSessionId = previousParentSessionId;
23993
+ child.terminalState = previousState;
23994
+ throw error51;
23995
+ }
23996
+ }
23997
+ resolveMaterialization(childThreadId, sessionId) {
23998
+ for (const resolve of this.materializationWaiters.get(childThreadId) ?? []) resolve(sessionId);
23999
+ this.materializationWaiters.delete(childThreadId);
24000
+ }
24001
+ hasOutstanding() {
24002
+ return this.pendingSpawns.size > 0 || [...this.children.values()].some((child) => child.terminalState === void 0);
24003
+ }
24004
+ notifyWaiters() {
24005
+ for (const waiter of this.waiters) waiter();
24006
+ this.waiters.clear();
24007
+ }
24008
+ parentForPath(path9) {
24009
+ const normalized = normalizeAgentPath(path9);
24010
+ const separator = normalized.lastIndexOf("/");
24011
+ if (separator <= 0) return { threadId: this.rootSessionId, sessionId: this.rootSessionId };
24012
+ const parentPath = normalized.slice(0, separator);
24013
+ const parent = [...this.children.entries()].find(([, child]) => child.path === parentPath);
24014
+ return parent ? { threadId: parent[0], sessionId: parent[1].sessionId } : { threadId: this.rootSessionId, sessionId: this.rootSessionId };
24015
+ }
24016
+ };
24017
+ function terminalStateOf(status) {
24018
+ switch (status) {
24019
+ case "completed":
24020
+ return "completed";
24021
+ case "interrupted":
24022
+ return "cancelled";
24023
+ case "errored":
24024
+ case "shutdown":
24025
+ case "notFound":
24026
+ return "failed";
24027
+ case "pendingInit":
24028
+ case "running":
24029
+ return void 0;
24030
+ }
24031
+ }
24032
+ function terminalStateFromTurn(status) {
24033
+ switch (status) {
24034
+ case "completed":
24035
+ return "completed";
24036
+ case "interrupted":
24037
+ return "cancelled";
24038
+ case "failed":
24039
+ return "failed";
24040
+ case "inProgress":
24041
+ return void 0;
24042
+ }
24043
+ }
24044
+ function fallbackName(sessionId) {
24045
+ const suffix = sessionId.length > 8 ? sessionId.slice(-8) : sessionId;
24046
+ return `Agent ${suffix}`;
24047
+ }
23624
24048
 
23625
24049
  // src/CodexEventHandler.ts
23626
24050
  var MAX_SESSION_FAILURE_TITLE_LENGTH = 240;
@@ -23728,13 +24152,18 @@ var CodexEventHandler = class _CodexEventHandler {
23728
24152
  terminalCommandIds = /* @__PURE__ */ new Set();
23729
24153
  terminalCommandOutputIds = /* @__PURE__ */ new Set();
23730
24154
  agentMessagePhases = /* @__PURE__ */ new Map();
23731
- activeSubAgentActivities = /* @__PURE__ */ new Set();
23732
- constructor(connection, sessionState, supportsPlanUpdates = false, supportsTypedSessionFailures = false, sessionFailureEpoch = randomUUID()) {
24155
+ subagents;
24156
+ constructor(connection, sessionState, supportsPlanUpdates = false, supportsTypedSessionFailures = false, sessionFailureEpoch = randomUUID(), subagents = new CodexSubagentEventRouter(
24157
+ sessionState.sessionId,
24158
+ false,
24159
+ new ACPSessionConnection(connection, sessionState.sessionId)
24160
+ )) {
23733
24161
  this.sessionState = sessionState;
23734
24162
  this.supportsPlanUpdates = supportsPlanUpdates;
23735
24163
  this.supportsTypedSessionFailures = supportsTypedSessionFailures;
23736
24164
  this.sessionFailureEpoch = sessionFailureEpoch;
23737
24165
  this.session = new ACPSessionConnection(connection, sessionState.sessionId);
24166
+ this.subagents = subagents;
23738
24167
  if (sessionState.sessionFailure !== void 0) {
23739
24168
  this.failuresById.set(sessionState.sessionFailure.id, sessionState.sessionFailure);
23740
24169
  }
@@ -23836,11 +24265,30 @@ var CodexEventHandler = class _CodexEventHandler {
23836
24265
  }
23837
24266
  async handleNotification(notification) {
23838
24267
  await this.flushPendingErrors();
24268
+ const handledBySubagents = await this.subagents.handle(notification);
24269
+ for (const buffered of this.subagents.takeBufferedNotifications()) {
24270
+ await this.handleNotification(buffered);
24271
+ }
24272
+ if (handledBySubagents) {
24273
+ return;
24274
+ }
24275
+ if (this.subagents.shouldIgnore(notification)) {
24276
+ return;
24277
+ }
23839
24278
  const updateEvent = await this.createUpdateEvent(notification);
23840
24279
  if (updateEvent) {
23841
- await this.session.update(updateEvent);
24280
+ await this.session.update(updateEvent, this.subagents.notificationSessionId(notification));
23842
24281
  }
23843
24282
  }
24283
+ async waitForNativeSubagentSession(childThreadId) {
24284
+ return await this.subagents.waitForMaterializedSession(childThreadId);
24285
+ }
24286
+ async waitForNativeSubagents(signal) {
24287
+ await this.subagents.wait(signal);
24288
+ }
24289
+ async finishOutstandingNativeSubagents(state) {
24290
+ await this.subagents.finishOutstanding(state);
24291
+ }
23844
24292
  async flushPendingPlanUpdates() {
23845
24293
  this.cancelPlanUpdateTimer();
23846
24294
  do {
@@ -24132,15 +24580,14 @@ ${event.details}` : event.summary;
24132
24580
  this.activeImageGenerationItems.add(event.item.id);
24133
24581
  return createImageGenerationStartUpdate(event.item);
24134
24582
  case "collabAgentToolCall":
24135
- return createCollabAgentToolCallUpdate(event.item);
24583
+ return this.subagents.legacyCollaborationStarted(event.item);
24136
24584
  case "agentMessage":
24137
24585
  this.rememberAgentMessagePhase(event.item);
24138
24586
  return null;
24139
24587
  case "contextCompaction":
24140
24588
  return createContextCompactionStartUpdate(event.item);
24141
24589
  case "subAgentActivity":
24142
- this.activeSubAgentActivities.add(event.item.id);
24143
- return createSubAgentActivityUpdate(event.item, "in_progress", "tool_call");
24590
+ return this.subagents.legacyActivityStarted(event.item);
24144
24591
  case "sleep":
24145
24592
  case "userMessage":
24146
24593
  case "hookPrompt":
@@ -24188,7 +24635,7 @@ ${event.details}` : event.summary;
24188
24635
  case "webSearch":
24189
24636
  return createWebSearchCompleteUpdate(event.item);
24190
24637
  case "collabAgentToolCall":
24191
- return createCollabAgentToolCallCompleteUpdate(event.item);
24638
+ return this.subagents.legacyCollaborationCompleted(event.item);
24192
24639
  case "agentMessage":
24193
24640
  this.rememberAgentMessagePhase(event.item);
24194
24641
  return null;
@@ -24201,10 +24648,8 @@ ${event.details}` : event.summary;
24201
24648
  case "contextCompaction":
24202
24649
  return createContextCompactionCompleteUpdate(event.item);
24203
24650
  //ignored types
24204
- case "subAgentActivity": {
24205
- const sessionUpdate = this.activeSubAgentActivities.delete(event.item.id) ? "tool_call_update" : "tool_call";
24206
- return createSubAgentActivityUpdate(event.item, "completed", sessionUpdate);
24207
- }
24651
+ case "subAgentActivity":
24652
+ return this.subagents.legacyActivityCompleted(event.item);
24208
24653
  case "sleep":
24209
24654
  case "userMessage":
24210
24655
  case "hookPrompt":
@@ -24653,343 +25098,545 @@ ${event.stdin}
24653
25098
  }
24654
25099
  };
24655
25100
 
24656
- // src/ApprovalOptionId.ts
25101
+ // src/permissions/option-ids.ts
24657
25102
  var ApprovalOptionId = {
24658
25103
  AllowOnce: "allow_once",
24659
- AllowAlways: "allow_always",
24660
- RejectOnce: "reject_once",
25104
+ AllowForSession: "allow_for_session",
25105
+ Decline: "decline",
25106
+ Cancel: "cancel",
24661
25107
  AcceptWithExecpolicyAmendment: "accept_execpolicy_amendment",
24662
25108
  ApplyNetworkPolicyAmendment: "apply_network_policy_amendment",
24663
25109
  AllowPermissionsForTurn: "allow_permissions_turn",
25110
+ AllowPermissionsForTurnWithStrictAutoReview: "allow_permissions_turn_strict_auto_review",
24664
25111
  AllowPermissionsForSession: "allow_permissions_session",
24665
25112
  RejectPermissions: "reject_permissions"
24666
25113
  };
25114
+ var McpApprovalOptionId = {
25115
+ AllowOnce: "allow_once",
25116
+ AllowSession: "allow_session",
25117
+ AllowAlways: "allow_always",
25118
+ Decline: "decline",
25119
+ Cancel: "cancel"
25120
+ };
24667
25121
 
24668
- // src/CodexApprovalHandler.ts
24669
- function permissionOption(optionId, name, kind, codexMeta, permission) {
24670
- return {
24671
- optionId,
24672
- name,
24673
- kind,
24674
- ...codexMeta || permission ? {
24675
- _meta: {
24676
- ...permission ? { permission } : {},
24677
- ...codexMeta ? { codex: codexMeta } : {}
24678
- }
24679
- } : {}
24680
- };
25122
+ // src/permissions/json.ts
25123
+ function isRecord2(value) {
25124
+ return value !== null && typeof value === "object" && !Array.isArray(value);
24681
25125
  }
24682
- var CodexApprovalHandler = class {
24683
- connection;
24684
- sessionState;
24685
- cancellationSignal;
24686
- constructor(connection, sessionState, cancellationSignal) {
24687
- this.connection = connection;
24688
- this.sessionState = sessionState;
24689
- this.cancellationSignal = cancellationSignal;
25126
+ function recordOrNull(value) {
25127
+ return isRecord2(value) ? value : null;
25128
+ }
25129
+ function normalizeJsonValue(value) {
25130
+ if (value === null || value === void 0) return null;
25131
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
25132
+ if (typeof value === "bigint") return Number(value);
25133
+ if (Array.isArray(value)) return value.map(normalizeJsonValue);
25134
+ if (typeof value === "object") {
25135
+ return Object.fromEntries(
25136
+ Object.entries(value).filter(([, nested]) => nested !== void 0).map(([key, nested]) => [key, normalizeJsonValue(nested)])
25137
+ );
24690
25138
  }
24691
- async handleCommandExecution(params) {
24692
- try {
24693
- const sessionId = this.sessionState.sessionId;
24694
- const acpRequest = this.buildCommandPermissionRequest(sessionId, params);
24695
- const response = await this.connection.request(
24696
- methods.client.session.requestPermission,
24697
- acpRequest,
24698
- this.requestOptions()
24699
- );
24700
- return this.convertCommandResponse(params, response);
24701
- } catch (error51) {
24702
- logger.error("Error requesting command execution permission", error51);
24703
- return { decision: "cancel" };
24704
- }
25139
+ return String(value);
25140
+ }
25141
+ function normalizeJsonObject(value) {
25142
+ return Object.fromEntries(
25143
+ Object.entries(value).filter(([, nested]) => nested !== void 0).map(([key, nested]) => [key, normalizeJsonValue(nested)])
25144
+ );
25145
+ }
25146
+
25147
+ // src/permissions/command-decision-contract.ts
25148
+ function parseAvailableCommandDecisions(params) {
25149
+ if (params.availableDecisions === void 0 || params.availableDecisions === null) {
25150
+ return defaultCommandDecisions(params);
25151
+ }
25152
+ if (!Array.isArray(params.availableDecisions) || params.availableDecisions.length === 0) return void 0;
25153
+ const decisions = [];
25154
+ for (const candidate of params.availableDecisions) {
25155
+ const decision = parseCommandDecision(candidate, params);
25156
+ if (!decision) return void 0;
25157
+ decisions.push(decision);
25158
+ }
25159
+ return decisions;
25160
+ }
25161
+ function defaultCommandDecisions(params) {
25162
+ if (params.networkApprovalContext) {
25163
+ const decisions2 = ["accept", "acceptForSession"];
25164
+ for (const amendment of params.proposedNetworkPolicyAmendments ?? []) {
25165
+ decisions2.push({ applyNetworkPolicyAmendment: { network_policy_amendment: amendment } });
25166
+ }
25167
+ decisions2.push("decline", "cancel");
25168
+ return decisions2;
25169
+ }
25170
+ if (params.additionalPermissions) return ["accept", "cancel"];
25171
+ const decisions = ["accept", "acceptForSession"];
25172
+ if (params.proposedExecpolicyAmendment && params.proposedExecpolicyAmendment.length > 0) {
25173
+ decisions.push({
25174
+ acceptWithExecpolicyAmendment: { execpolicy_amendment: params.proposedExecpolicyAmendment }
25175
+ });
24705
25176
  }
24706
- async handleFileChange(params) {
24707
- try {
24708
- const sessionId = this.sessionState.sessionId;
24709
- const acpRequest = this.buildFileChangePermissionRequest(sessionId, params);
24710
- const response = await this.connection.request(
24711
- methods.client.session.requestPermission,
24712
- acpRequest,
24713
- this.requestOptions()
24714
- );
24715
- return this.convertFileChangeResponse(params, response);
24716
- } catch (error51) {
24717
- logger.error("Error requesting file change permission", error51);
24718
- return { decision: "cancel" };
25177
+ decisions.push("decline", "cancel");
25178
+ return decisions;
25179
+ }
25180
+ function parseCommandDecision(candidate, params) {
25181
+ if (candidate === "accept" || candidate === "acceptForSession" || candidate === "decline" || candidate === "cancel") {
25182
+ return candidate;
25183
+ }
25184
+ if (!isRecord2(candidate)) return void 0;
25185
+ if ("acceptWithExecpolicyAmendment" in candidate) {
25186
+ const value = candidate["acceptWithExecpolicyAmendment"];
25187
+ if (!isRecord2(value)) return void 0;
25188
+ const amendment = value["execpolicy_amendment"];
25189
+ if (!isStringArray(amendment) || amendment.length === 0) return void 0;
25190
+ if (!sameStrings(amendment, params.proposedExecpolicyAmendment)) return void 0;
25191
+ return { acceptWithExecpolicyAmendment: { execpolicy_amendment: [...amendment] } };
25192
+ }
25193
+ if ("applyNetworkPolicyAmendment" in candidate) {
25194
+ const value = candidate["applyNetworkPolicyAmendment"];
25195
+ if (!isRecord2(value)) return void 0;
25196
+ const amendment = parseNetworkAmendment(value["network_policy_amendment"]);
25197
+ if (!amendment || !params.networkApprovalContext) return void 0;
25198
+ if (amendment.host !== params.networkApprovalContext.host) return void 0;
25199
+ if (!(params.proposedNetworkPolicyAmendments ?? []).some((proposed) => sameNetworkAmendment(proposed, amendment))) {
25200
+ return void 0;
24719
25201
  }
25202
+ return { applyNetworkPolicyAmendment: { network_policy_amendment: amendment } };
24720
25203
  }
24721
- async handlePermissionsRequest(params) {
24722
- try {
24723
- const sessionId = this.sessionState.sessionId;
24724
- const acpRequest = this.buildPermissionsRequest(sessionId, params);
24725
- const response = await this.connection.request(
24726
- methods.client.session.requestPermission,
24727
- acpRequest,
24728
- this.requestOptions()
24729
- );
24730
- return this.convertPermissionsResponse(params, response);
24731
- } catch (error51) {
24732
- logger.error("Error requesting permissions", error51);
24733
- return this.rejectPermissionsResponse();
25204
+ return void 0;
25205
+ }
25206
+ function parseNetworkAmendment(value) {
25207
+ if (!isRecord2(value) || typeof value["host"] !== "string") return void 0;
25208
+ const action = value["action"];
25209
+ if (action !== "allow" && action !== "deny") return void 0;
25210
+ return { host: value["host"], action };
25211
+ }
25212
+ function isStringArray(value) {
25213
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
25214
+ }
25215
+ function sameStrings(left, right) {
25216
+ return !!right && left.length === right.length && left.every((value, index) => value === right[index]);
25217
+ }
25218
+ function sameNetworkAmendment(left, right) {
25219
+ return left.host === right.host && left.action === right.action;
25220
+ }
25221
+
25222
+ // src/permissions/options.ts
25223
+ function commandDecisionOptions(params) {
25224
+ const decisions = parseAvailableCommandDecisions(params);
25225
+ if (!decisions) return void 0;
25226
+ const options = [];
25227
+ let networkIndex = 0;
25228
+ for (const decision of decisions) {
25229
+ if (decision === "accept") {
25230
+ options.push(decisionOption(
25231
+ ApprovalOptionId.AllowOnce,
25232
+ params.networkApprovalContext ? "Yes, just this once" : "Yes, proceed",
25233
+ "allow_once",
25234
+ decision
25235
+ ));
25236
+ } else if (decision === "acceptForSession") {
25237
+ options.push(decisionOption(
25238
+ ApprovalOptionId.AllowForSession,
25239
+ params.networkApprovalContext ? "Yes, and allow this host for this conversation" : params.additionalPermissions ? "Yes, and allow these permissions for this session" : "Yes, and don't ask again for this command in this session",
25240
+ "allow_always",
25241
+ decision
25242
+ ));
25243
+ } else if (decision === "decline") {
25244
+ options.push(decisionOption(
25245
+ ApprovalOptionId.Decline,
25246
+ "No, continue without running it",
25247
+ "reject_once",
25248
+ decision
25249
+ ));
25250
+ } else if (decision === "cancel") {
25251
+ options.push(decisionOption(
25252
+ ApprovalOptionId.Cancel,
25253
+ "No, and tell Codex what to do differently",
25254
+ "reject_once",
25255
+ decision
25256
+ ));
25257
+ } else if ("acceptWithExecpolicyAmendment" in decision) {
25258
+ const prefix = renderExecPolicyPrefix(decision.acceptWithExecpolicyAmendment.execpolicy_amendment);
25259
+ if (prefix.includes("\n") || prefix.includes("\r")) continue;
25260
+ options.push(decisionOption(
25261
+ ApprovalOptionId.AcceptWithExecpolicyAmendment,
25262
+ `Yes, and don't ask again for commands that start with \`${prefix}\``,
25263
+ "allow_always",
25264
+ decision
25265
+ ));
25266
+ } else {
25267
+ const amendment = decision.applyNetworkPolicyAmendment.network_policy_amendment;
25268
+ options.push(decisionOption(
25269
+ `${ApprovalOptionId.ApplyNetworkPolicyAmendment}:${networkIndex++}`,
25270
+ amendment.action === "allow" ? "Yes, and allow this host in the future" : "No, and block this host in the future",
25271
+ amendment.action === "allow" ? "allow_always" : "reject_always",
25272
+ decision
25273
+ ));
24734
25274
  }
24735
25275
  }
24736
- requestOptions() {
24737
- return this.cancellationSignal ? { cancellationSignal: this.cancellationSignal } : void 0;
25276
+ const orderedOptions = [...options].sort(
25277
+ (left, right) => permissionOptionOrder(left.option) - permissionOptionOrder(right.option)
25278
+ );
25279
+ const hasAllow = orderedOptions.some(({ option }) => option.kind === "allow_once" || option.kind === "allow_always");
25280
+ const hasReject = orderedOptions.some(({ option }) => option.kind === "reject_once" || option.kind === "reject_always");
25281
+ const optionIds = orderedOptions.map(({ option }) => option.optionId);
25282
+ const hasUniqueOptionIds = new Set(optionIds).size === optionIds.length;
25283
+ return hasAllow && hasReject && hasUniqueOptionIds ? orderedOptions : void 0;
25284
+ }
25285
+ function permissionOptionOrder(option) {
25286
+ if (option.kind === "allow_once") return 0;
25287
+ if (option.kind === "allow_always") return 1;
25288
+ return 2;
25289
+ }
25290
+ function fileChangeDecisionOptions() {
25291
+ return [
25292
+ decisionOption(ApprovalOptionId.AllowOnce, "Yes, proceed", "allow_once", "accept"),
25293
+ decisionOption(
25294
+ ApprovalOptionId.AllowForSession,
25295
+ "Yes, and don't ask again for these files",
25296
+ "allow_always",
25297
+ "acceptForSession"
25298
+ ),
25299
+ decisionOption(
25300
+ ApprovalOptionId.Cancel,
25301
+ "No, and tell Codex what to do differently",
25302
+ "reject_once",
25303
+ "cancel"
25304
+ )
25305
+ ];
25306
+ }
25307
+ function permissionProfileOptions() {
25308
+ return [
25309
+ permissionOption(
25310
+ ApprovalOptionId.AllowPermissionsForTurn,
25311
+ "Yes, grant these permissions for this turn",
25312
+ "allow_once"
25313
+ ),
25314
+ permissionOption(
25315
+ ApprovalOptionId.AllowPermissionsForTurnWithStrictAutoReview,
25316
+ "Yes, grant for this turn with strict auto review",
25317
+ "allow_once"
25318
+ ),
25319
+ permissionOption(
25320
+ ApprovalOptionId.AllowPermissionsForSession,
25321
+ "Yes, grant these permissions for this session",
25322
+ "allow_always"
25323
+ ),
25324
+ permissionOption(
25325
+ ApprovalOptionId.RejectPermissions,
25326
+ "No, continue without permissions",
25327
+ "reject_once"
25328
+ )
25329
+ ];
25330
+ }
25331
+ function decisionOption(optionId, name, kind, decision) {
25332
+ return { option: permissionOption(optionId, name, kind), decision };
25333
+ }
25334
+ function permissionOption(optionId, name, kind) {
25335
+ return { optionId, name, kind };
25336
+ }
25337
+ function renderExecPolicyPrefix(command) {
25338
+ const script = extractWrappedScript(command);
25339
+ if (script !== void 0) return script;
25340
+ if (command.some((value) => value.includes("\0"))) return command.join(" ");
25341
+ return command.map(shlexQuote).join(" ");
25342
+ }
25343
+ function extractWrappedScript(command) {
25344
+ const executable = executableName(command[0]);
25345
+ if ((executable === "bash" || executable === "zsh" || executable === "sh") && command.length === 3 && (command[1] === "-lc" || command[1] === "-c")) {
25346
+ return command[2];
24738
25347
  }
24739
- buildCommandPermissionRequest(sessionId, params) {
24740
- const options = this.buildCommandOptions(params).map(({ option }) => option);
24741
- return {
24742
- sessionId,
24743
- toolCall: {
24744
- toolCallId: params.itemId,
24745
- kind: "execute",
24746
- status: "pending",
24747
- rawInput: params.command ? { command: stripShellPrefix(params.command), cwd: params.cwd } : null
24748
- },
24749
- options,
24750
- _meta: { codex: { params } }
24751
- };
25348
+ if (executable !== "pwsh" && executable !== "powershell") return void 0;
25349
+ const allowedFlags = /* @__PURE__ */ new Set(["-nologo", "-noprofile", "-command", "-c"]);
25350
+ for (let index = 1; index + 1 < command.length; index++) {
25351
+ const flag = command[index]?.toLowerCase();
25352
+ if (flag === void 0 || !allowedFlags.has(flag)) return void 0;
25353
+ if (flag === "-command" || flag === "-c") return command[index + 1];
24752
25354
  }
24753
- buildFileChangePermissionRequest(sessionId, params) {
24754
- const options = this.buildFileChangeOptions(params).map(({ option }) => option);
24755
- return {
24756
- sessionId,
24757
- toolCall: {
24758
- toolCallId: params.itemId,
24759
- kind: "edit",
24760
- status: "pending"
24761
- },
24762
- options,
24763
- _meta: { codex: { params } }
24764
- };
25355
+ return void 0;
25356
+ }
25357
+ function executableName(command) {
25358
+ let filename = command?.replaceAll("\\", "/").split("/").at(-1);
25359
+ while (filename !== void 0) {
25360
+ if (["bash", "zsh", "sh", "pwsh", "powershell"].includes(filename)) return filename;
25361
+ const dot = filename.lastIndexOf(".");
25362
+ if (dot <= 0) return void 0;
25363
+ filename = filename.slice(0, dot);
24765
25364
  }
24766
- buildPermissionsRequest(sessionId, params) {
24767
- const content = this.createContent([
24768
- params.reason,
24769
- this.formatRequestedPermissions(params.permissions)
24770
- ]);
24771
- return {
24772
- sessionId,
24773
- toolCall: {
24774
- toolCallId: params.itemId,
24775
- kind: "other",
24776
- status: "pending",
24777
- title: params.reason ?? "Permissions Request",
24778
- rawInput: params,
24779
- ...content ? { content } : {}
24780
- },
24781
- options: [
24782
- permissionOption(
24783
- ApprovalOptionId.AllowPermissionsForSession,
24784
- "Allow for Session",
24785
- "allow_always",
24786
- { decision: "allowPermissionsForSession", permissions: params.permissions },
24787
- this.permissionGrantMetadata(params.permissions, "session")
24788
- ),
24789
- permissionOption(
24790
- ApprovalOptionId.AllowPermissionsForTurn,
24791
- "Allow Once",
24792
- "allow_once",
24793
- { decision: "allowPermissionsForTurn", permissions: params.permissions },
24794
- this.permissionGrantMetadata(params.permissions, "turn")
24795
- ),
24796
- permissionOption(
24797
- ApprovalOptionId.RejectPermissions,
24798
- "Reject",
24799
- "reject_once",
24800
- { decision: "rejectPermissions" }
24801
- )
24802
- ],
24803
- _meta: { codex: { params } }
24804
- };
25365
+ return void 0;
25366
+ }
25367
+ function shlexQuote(value) {
25368
+ if (value.length === 0) return "''";
25369
+ const UNQUOTED = 1;
25370
+ const SINGLE_QUOTED = 2;
25371
+ const DOUBLE_QUOTED = 4;
25372
+ let offset = 0;
25373
+ let result = "";
25374
+ while (offset < value.length) {
25375
+ const start = offset;
25376
+ let allowed = UNQUOTED | SINGLE_QUOTED | DOUBLE_QUOTED;
25377
+ if (value[offset] === "^") {
25378
+ allowed = SINGLE_QUOTED;
25379
+ offset++;
25380
+ }
25381
+ while (offset < value.length) {
25382
+ const character = value[offset];
25383
+ let nextAllowed = allowed;
25384
+ if (!isShlexUnquoted(character)) nextAllowed &= ~UNQUOTED;
25385
+ if (character === "'" || character === "^" || character === "\\") nextAllowed &= ~SINGLE_QUOTED;
25386
+ if (character === "`" || character === "$" || character === "!" || character === "^") {
25387
+ nextAllowed &= ~DOUBLE_QUOTED;
25388
+ }
25389
+ if (nextAllowed === 0) break;
25390
+ allowed = nextAllowed;
25391
+ offset++;
25392
+ }
25393
+ const chunk = value.slice(start, offset);
25394
+ if ((allowed & UNQUOTED) !== 0) result += chunk;
25395
+ else if ((allowed & SINGLE_QUOTED) !== 0) result += `'${chunk}'`;
25396
+ else result += `"${chunk.replace(/["\\]/g, "\\$&")}"`;
25397
+ }
25398
+ return result;
25399
+ }
25400
+ function isShlexUnquoted(character) {
25401
+ return /^[0-9A-Za-z]$/.test(character) || "+-./:@]_".includes(character);
25402
+ }
25403
+
25404
+ // src/permissions/metadata.ts
25405
+ var CODEX_COMMAND_PERMISSION_TITLE = "Run command?";
25406
+ var CODEX_NETWORK_PERMISSION_TITLE = "Allow network access?";
25407
+ var CODEX_FILE_CHANGE_PERMISSION_TITLE = "Make edits?";
25408
+ var CODEX_ADDITIONAL_PERMISSIONS_TITLE = "Grant permissions?";
25409
+ function requestPermissionMeta(title, reason) {
25410
+ const description = nonBlank(reason);
25411
+ const permission = {
25412
+ version: 1,
25413
+ title,
25414
+ ...description ? { description } : {}
25415
+ };
25416
+ return { permission };
25417
+ }
25418
+ function optionPermissionMeta(description) {
25419
+ const normalized = nonBlank(description);
25420
+ if (!normalized) return void 0;
25421
+ const permission = { version: 1, description: normalized };
25422
+ return { permission };
25423
+ }
25424
+ function nonBlank(value) {
25425
+ const normalized = value?.trim();
25426
+ return normalized ? normalized : void 0;
25427
+ }
25428
+
25429
+ // src/permissions/presentation.ts
25430
+ function commandToolCall(params) {
25431
+ const network = params.networkApprovalContext;
25432
+ const rawInput = {
25433
+ ...params.command ? { command: stripShellPrefix(params.command) } : {},
25434
+ ...params.cwd ? { cwd: params.cwd } : {},
25435
+ ...network?.protocol === "http" || network?.protocol === "https" ? { url: `${network.protocol}://${network.host}` } : {},
25436
+ ...params.additionalPermissions ? { additionalPermissions: params.additionalPermissions } : {}
25437
+ };
25438
+ const additionalPermissionContent = params.additionalPermissions ? permissionProfileContent(params.additionalPermissions) : [];
25439
+ return {
25440
+ toolCallId: params.itemId,
25441
+ kind: "execute",
25442
+ status: "pending",
25443
+ title: network ? `${network.protocol} network access to ${network.host}` : commandTitle(params.commandActions),
25444
+ ...Object.keys(rawInput).length > 0 ? { rawInput } : {},
25445
+ ...locationsField(unique([
25446
+ ...commandActionPaths(params.commandActions),
25447
+ ...permissionProfilePaths(params.additionalPermissions)
25448
+ ])),
25449
+ ...network ? { content: [textContent(`${network.protocol} access to ${network.host}`), ...additionalPermissionContent] } : additionalPermissionContent.length > 0 ? { content: additionalPermissionContent } : {}
25450
+ };
25451
+ }
25452
+ function fileChangeToolCall(params, permissionContext) {
25453
+ const item = permissionContext.fileChange(params.threadId, params.itemId);
25454
+ return {
25455
+ toolCallId: params.itemId,
25456
+ kind: "edit",
25457
+ status: "pending",
25458
+ title: "Edit files",
25459
+ ...locationsField(fileChangePaths(item))
25460
+ };
25461
+ }
25462
+ function additionalPermissionsToolCall(itemId, cwd, environmentId, permissions) {
25463
+ const content = permissionProfileContent(permissions);
25464
+ return {
25465
+ toolCallId: itemId,
25466
+ kind: "other",
25467
+ status: "pending",
25468
+ title: "Additional sandbox permissions",
25469
+ rawInput: { permissions, cwd, environmentId },
25470
+ ...locationsField(permissionProfilePaths(permissions)),
25471
+ ...content.length > 0 ? { content } : {}
25472
+ };
25473
+ }
25474
+ function commandTitle(actions) {
25475
+ const first = actions?.[0];
25476
+ if (!first) return "Run command";
25477
+ switch (first.type) {
25478
+ case "read":
25479
+ return actions?.length === 1 ? "Read file" : "Run command with file reads";
25480
+ case "listFiles":
25481
+ return "List files";
25482
+ case "search":
25483
+ return "Search files";
25484
+ case "unknown":
25485
+ return "Run command";
25486
+ }
25487
+ }
25488
+ function commandActionPaths(actions) {
25489
+ return unique((actions ?? []).flatMap((action) => {
25490
+ switch (action.type) {
25491
+ case "read":
25492
+ return [action.path];
25493
+ case "listFiles":
25494
+ case "search":
25495
+ return action.path ? [action.path] : [];
25496
+ case "unknown":
25497
+ return [];
25498
+ }
25499
+ }));
25500
+ }
25501
+ function fileChangePaths(item) {
25502
+ return unique(item?.changes.map((change) => change.path) ?? []);
25503
+ }
25504
+ function permissionProfilePaths(permissions) {
25505
+ const fileSystem = permissions?.fileSystem;
25506
+ return unique([
25507
+ ...fileSystem?.read ?? [],
25508
+ ...fileSystem?.write ?? [],
25509
+ ...(fileSystem?.entries ?? []).flatMap((entry) => entry.path.type === "path" ? [entry.path.path] : [])
25510
+ ]);
25511
+ }
25512
+ function permissionProfileContent(permissions) {
25513
+ const lines = [];
25514
+ const networkEnabled = permissions.network?.enabled;
25515
+ if (networkEnabled !== null && networkEnabled !== void 0) {
25516
+ lines.push(networkEnabled ? "Enable network access" : "Disable network access");
25517
+ }
25518
+ for (const entry of permissions.fileSystem?.entries ?? []) {
25519
+ switch (entry.path.type) {
25520
+ case "glob_pattern":
25521
+ lines.push(`${entry.access} filesystem pattern ${entry.path.pattern}`);
25522
+ break;
25523
+ case "special":
25524
+ lines.push(`${entry.access} Codex filesystem scope ${JSON.stringify(entry.path.value)}`);
25525
+ break;
25526
+ case "path":
25527
+ break;
25528
+ }
25529
+ }
25530
+ return lines.length > 0 ? [textContent(lines.join("\n"))] : [];
25531
+ }
25532
+ function locationsField(paths) {
25533
+ return paths.length > 0 ? { locations: paths.map((path9) => ({ path: path9 })) } : {};
25534
+ }
25535
+ function textContent(text) {
25536
+ return { type: "content", content: { type: "text", text } };
25537
+ }
25538
+ function unique(values) {
25539
+ return [...new Set(values)];
25540
+ }
25541
+
25542
+ // src/permissions/CodexApprovalHandler.ts
25543
+ var CodexApprovalHandler = class {
25544
+ constructor(connection, permissionContext, cancellationSignal) {
25545
+ this.connection = connection;
25546
+ this.permissionContext = permissionContext;
25547
+ this.cancellationSignal = cancellationSignal;
24805
25548
  }
24806
- convertCommandResponse(params, response) {
24807
- if (response.outcome.outcome === "cancelled") {
25549
+ connection;
25550
+ permissionContext;
25551
+ cancellationSignal;
25552
+ async handleCommandExecution(params) {
25553
+ const authoritativeParams = params;
25554
+ const decisions = commandDecisionOptions(authoritativeParams);
25555
+ if (!decisions) {
25556
+ logger.error("Cancelling command approval without a complete authoritative decision set", void 0);
25557
+ return { decision: "cancel" };
25558
+ }
25559
+ try {
25560
+ const response = await this.requestPermission({
25561
+ sessionId: params.threadId,
25562
+ toolCall: commandToolCall(authoritativeParams),
25563
+ options: decisions.map(({ option }) => option),
25564
+ _meta: requestPermissionMeta(
25565
+ params.networkApprovalContext ? CODEX_NETWORK_PERMISSION_TITLE : CODEX_COMMAND_PERMISSION_TITLE,
25566
+ params.reason
25567
+ )
25568
+ });
25569
+ return { decision: this.selectedDecision(response, decisions) ?? "cancel" };
25570
+ } catch (error51) {
25571
+ logger.error("Error requesting command execution permission", error51);
24808
25572
  return { decision: "cancel" };
24809
25573
  }
24810
- const optionId = response.outcome.optionId;
24811
- const decision = this.buildCommandOptions(params).find(({ option }) => option.optionId === optionId)?.decision;
24812
- return { decision: decision ?? "decline" };
24813
25574
  }
24814
- convertFileChangeResponse(params, response) {
24815
- if (response.outcome.outcome === "cancelled") {
25575
+ async handleFileChange(params) {
25576
+ const decisions = fileChangeDecisionOptions();
25577
+ try {
25578
+ const response = await this.requestPermission({
25579
+ sessionId: params.threadId,
25580
+ toolCall: fileChangeToolCall(params, this.permissionContext),
25581
+ options: decisions.map(({ option }) => option),
25582
+ _meta: requestPermissionMeta(CODEX_FILE_CHANGE_PERMISSION_TITLE, params.reason)
25583
+ });
25584
+ return { decision: this.selectedDecision(response, decisions) ?? "cancel" };
25585
+ } catch (error51) {
25586
+ logger.error("Error requesting file change permission", error51);
24816
25587
  return { decision: "cancel" };
24817
25588
  }
24818
- const optionId = response.outcome.optionId;
24819
- const decision = this.buildFileChangeOptions(params).find(({ option }) => option.optionId === optionId)?.decision;
24820
- return { decision: decision ?? "decline" };
24821
25589
  }
24822
- convertPermissionsResponse(params, response) {
24823
- if (response.outcome.outcome === "cancelled") {
25590
+ async handlePermissionsRequest(params) {
25591
+ try {
25592
+ const response = await this.requestPermission({
25593
+ sessionId: params.threadId,
25594
+ toolCall: additionalPermissionsToolCall(
25595
+ params.itemId,
25596
+ params.cwd,
25597
+ params.environmentId,
25598
+ params.permissions
25599
+ ),
25600
+ options: permissionProfileOptions(),
25601
+ _meta: requestPermissionMeta(CODEX_ADDITIONAL_PERMISSIONS_TITLE, params.reason)
25602
+ });
25603
+ return this.permissionsResponse(params.permissions, response);
25604
+ } catch (error51) {
25605
+ logger.error("Error requesting permissions", error51);
24824
25606
  return this.rejectPermissionsResponse();
24825
25607
  }
25608
+ }
25609
+ requestPermission(request) {
25610
+ return this.connection.request(
25611
+ methods.client.session.requestPermission,
25612
+ request,
25613
+ this.cancellationSignal ? { cancellationSignal: this.cancellationSignal } : void 0
25614
+ );
25615
+ }
25616
+ selectedDecision(response, decisions) {
25617
+ if (response.outcome.outcome === "cancelled") return void 0;
25618
+ const optionId = response.outcome.optionId;
25619
+ return decisions.find(({ option }) => option.optionId === optionId)?.decision;
25620
+ }
25621
+ permissionsResponse(permissions, response) {
25622
+ if (response.outcome.outcome === "cancelled") return this.rejectPermissionsResponse();
24826
25623
  switch (response.outcome.optionId) {
24827
- case ApprovalOptionId.AllowPermissionsForSession:
24828
- case ApprovalOptionId.AllowAlways:
24829
- return {
24830
- permissions: this.grantedPermissions(params.permissions),
24831
- scope: "session",
24832
- strictAutoReview: false
24833
- };
24834
25624
  case ApprovalOptionId.AllowPermissionsForTurn:
24835
- case ApprovalOptionId.AllowOnce:
24836
- return {
24837
- permissions: this.grantedPermissions(params.permissions),
24838
- scope: "turn",
24839
- strictAutoReview: false
24840
- };
25625
+ return this.grantedPermissionsResponse(permissions, "turn", false);
25626
+ case ApprovalOptionId.AllowPermissionsForTurnWithStrictAutoReview:
25627
+ return this.grantedPermissionsResponse(permissions, "turn", true);
25628
+ case ApprovalOptionId.AllowPermissionsForSession:
25629
+ return this.grantedPermissionsResponse(permissions, "session", false);
25630
+ case ApprovalOptionId.RejectPermissions:
24841
25631
  default:
24842
25632
  return this.rejectPermissionsResponse();
24843
25633
  }
24844
25634
  }
24845
- buildCommandOptions(params) {
24846
- const options = [
24847
- {
24848
- option: permissionOption(ApprovalOptionId.AllowOnce, "Allow Once", "allow_once", { decision: "accept" }),
24849
- decision: "accept"
24850
- },
24851
- {
24852
- option: permissionOption(
24853
- ApprovalOptionId.AllowAlways,
24854
- params.networkApprovalContext ? "Allow Host for Session" : "Allow for Session",
24855
- "allow_always",
24856
- { decision: "acceptForSession" },
24857
- params.networkApprovalContext ? {
24858
- version: 1,
24859
- changes: [{
24860
- type: "grant",
24861
- operation: "grant",
24862
- description: `Allow access to ${params.networkApprovalContext.host} for this session`,
24863
- lifetime: { scope: "session" },
24864
- targets: [{
24865
- type: "network",
24866
- matcher: {
24867
- type: "host",
24868
- host: params.networkApprovalContext.host,
24869
- protocol: params.networkApprovalContext.protocol
24870
- }
24871
- }]
24872
- }]
24873
- } : void 0
24874
- ),
24875
- decision: "acceptForSession"
24876
- }
24877
- ];
24878
- if (params.proposedExecpolicyAmendment && params.proposedExecpolicyAmendment.length > 0) {
24879
- options.push({
24880
- option: permissionOption(
24881
- ApprovalOptionId.AcceptWithExecpolicyAmendment,
24882
- this.execpolicyAmendmentLabel(params.proposedExecpolicyAmendment),
24883
- "allow_always",
24884
- {
24885
- decision: "acceptWithExecpolicyAmendment",
24886
- execpolicyAmendment: params.proposedExecpolicyAmendment
24887
- },
24888
- {
24889
- version: 1,
24890
- changes: [{
24891
- type: "policy_rule",
24892
- operation: "add",
24893
- ruleBehavior: "allow",
24894
- description: `Allow commands starting with ${params.proposedExecpolicyAmendment.join(" ")}`,
24895
- targets: [{
24896
- type: "command",
24897
- matcher: {
24898
- type: "argv_prefix",
24899
- argv: params.proposedExecpolicyAmendment
24900
- }
24901
- }]
24902
- }]
24903
- }
24904
- ),
24905
- decision: {
24906
- acceptWithExecpolicyAmendment: {
24907
- execpolicy_amendment: params.proposedExecpolicyAmendment
24908
- }
24909
- }
24910
- });
24911
- }
24912
- params.proposedNetworkPolicyAmendments?.forEach((amendment, index) => {
24913
- options.push({
24914
- option: permissionOption(
24915
- this.networkPolicyAmendmentOptionId(index),
24916
- this.networkPolicyAmendmentLabel(amendment),
24917
- amendment.action === "allow" ? "allow_always" : "reject_always",
24918
- {
24919
- decision: "applyNetworkPolicyAmendment",
24920
- networkPolicyAmendment: amendment
24921
- },
24922
- {
24923
- version: 1,
24924
- changes: [{
24925
- type: "policy_rule",
24926
- operation: "add",
24927
- ruleBehavior: amendment.action,
24928
- description: amendment.action === "allow" ? `Allow access to ${amendment.host}` : `Block access to ${amendment.host}`,
24929
- targets: [{
24930
- type: "network",
24931
- matcher: {
24932
- type: "host",
24933
- host: amendment.host
24934
- }
24935
- }]
24936
- }]
24937
- }
24938
- ),
24939
- decision: {
24940
- applyNetworkPolicyAmendment: {
24941
- network_policy_amendment: amendment
24942
- }
24943
- }
24944
- });
24945
- });
24946
- options.push({
24947
- option: permissionOption(ApprovalOptionId.RejectOnce, "Reject", "reject_once", { decision: "decline" }),
24948
- decision: "decline"
24949
- });
24950
- return options;
24951
- }
24952
- buildFileChangeOptions(params) {
24953
- return [
24954
- {
24955
- option: permissionOption(ApprovalOptionId.AllowOnce, "Allow Once", "allow_once", { decision: "accept" }),
24956
- decision: "accept"
24957
- },
24958
- {
24959
- option: permissionOption(
24960
- ApprovalOptionId.AllowAlways,
24961
- params.grantRoot ? "Allow Root for Session" : "Allow for Session",
24962
- "allow_always",
24963
- { decision: "acceptForSession", grantRoot: params.grantRoot ?? null },
24964
- params.grantRoot ? {
24965
- version: 1,
24966
- changes: [{
24967
- type: "grant",
24968
- operation: "grant",
24969
- description: `Allow writes under ${params.grantRoot} for this session`,
24970
- lifetime: { scope: "session" },
24971
- targets: [{
24972
- type: "filesystem",
24973
- access: ["write"],
24974
- matcher: { type: "directory", path: params.grantRoot }
24975
- }]
24976
- }]
24977
- } : void 0
24978
- ),
24979
- decision: "acceptForSession"
24980
- },
24981
- {
24982
- option: permissionOption(ApprovalOptionId.RejectOnce, "Reject", "reject_once", { decision: "decline" }),
24983
- decision: "decline"
24984
- }
24985
- ];
25635
+ grantedPermissionsResponse(permissions, scope, strictAutoReview) {
25636
+ return { permissions: this.grantedPermissions(permissions), scope, strictAutoReview };
24986
25637
  }
24987
25638
  rejectPermissionsResponse() {
24988
- return {
24989
- permissions: {},
24990
- scope: "turn",
24991
- strictAutoReview: true
24992
- };
25639
+ return { permissions: {}, scope: "turn", strictAutoReview: false };
24993
25640
  }
24994
25641
  grantedPermissions(permissions) {
24995
25642
  return {
@@ -24997,117 +25644,131 @@ var CodexApprovalHandler = class {
24997
25644
  ...permissions.fileSystem ? { fileSystem: permissions.fileSystem } : {}
24998
25645
  };
24999
25646
  }
25000
- permissionGrantMetadata(permissions, scope) {
25001
- const changes = [];
25002
- const lifetime = { scope };
25003
- const suffix = scope === "session" ? " for this session" : " for this turn";
25004
- if (permissions.network?.enabled !== null && permissions.network?.enabled !== void 0) {
25005
- const allowed = permissions.network.enabled;
25006
- changes.push({
25007
- type: allowed ? "grant" : "policy_rule",
25008
- operation: allowed ? "grant" : "add",
25009
- ...allowed ? {} : { ruleBehavior: "deny" },
25010
- description: `${allowed ? "Allow" : "Deny"} network access${suffix}`,
25011
- lifetime,
25012
- targets: [{ type: "network", matcher: { type: "any" } }]
25013
- });
25014
- }
25015
- const fileSystem = permissions.fileSystem;
25016
- for (const path8 of fileSystem?.read ?? []) {
25017
- changes.push(this.fileSystemGrantChange(path8, "read", lifetime, suffix));
25018
- }
25019
- for (const path8 of fileSystem?.write ?? []) {
25020
- changes.push(this.fileSystemGrantChange(path8, "write", lifetime, suffix));
25021
- }
25022
- for (const entry of fileSystem?.entries ?? []) {
25023
- const matcher = (() => {
25024
- switch (entry.path.type) {
25025
- case "path":
25026
- return { type: "exact_path", path: entry.path.path };
25027
- case "glob_pattern":
25028
- return { type: "glob", pattern: entry.path.pattern };
25029
- case "special":
25030
- return { type: "special", provider: "codex", value: entry.path.value };
25031
- }
25032
- })();
25033
- const pathDescription = entry.path.type === "path" ? entry.path.path : entry.path.type === "glob_pattern" ? entry.path.pattern : JSON.stringify(entry.path.value);
25034
- changes.push({
25035
- type: entry.access === "deny" ? "policy_rule" : "grant",
25036
- operation: entry.access === "deny" ? "add" : "grant",
25037
- ...entry.access === "deny" ? { ruleBehavior: "deny" } : {},
25038
- description: entry.access === "deny" ? `Deny filesystem access to ${pathDescription}${suffix}` : `Allow ${entry.access} access to ${pathDescription}${suffix}`,
25039
- lifetime,
25040
- targets: [{
25041
- type: "filesystem",
25042
- ...entry.access === "deny" ? {} : { access: [entry.access] },
25043
- matcher
25044
- }]
25045
- });
25046
- }
25047
- return changes.length > 0 ? { version: 1, changes } : void 0;
25647
+ };
25648
+
25649
+ // src/permissions/lifecycle.ts
25650
+ var PermissionLifecycleContext = class {
25651
+ constructor(session) {
25652
+ this.session = session;
25048
25653
  }
25049
- fileSystemGrantChange(path8, access, lifetime, suffix) {
25050
- return {
25051
- type: "grant",
25052
- operation: "grant",
25053
- description: `Allow ${access} access to ${path8}${suffix}`,
25054
- lifetime,
25055
- targets: [{
25056
- type: "filesystem",
25057
- access: [access],
25058
- matcher: { type: "exact_path", path: path8 }
25059
- }]
25060
- };
25654
+ session;
25655
+ permissionRequestSequence = 0;
25656
+ beginPrompt() {
25657
+ return new PermissionPromptContext((serverName) => this.nextStandaloneMcpToolCallId(serverName));
25061
25658
  }
25062
- networkPolicyAmendmentOptionId(index) {
25063
- return `${ApprovalOptionId.ApplyNetworkPolicyAmendment}:${index}`;
25659
+ nextStandaloneMcpToolCallId(serverName) {
25660
+ this.permissionRequestSequence += 1;
25661
+ return ["elicitation", this.session.sessionId, serverName, this.permissionRequestSequence].join(":");
25064
25662
  }
25065
- execpolicyAmendmentLabel(amendment) {
25066
- const commandPrefix = amendment.join(" ");
25067
- if (!commandPrefix || commandPrefix.includes("\n") || commandPrefix.includes("\r")) {
25068
- return "Allow and Remember Command Pattern";
25663
+ };
25664
+ var PermissionPromptContext = class {
25665
+ constructor(nextStandaloneId) {
25666
+ this.nextStandaloneId = nextStandaloneId;
25667
+ }
25668
+ nextStandaloneId;
25669
+ fileChanges = /* @__PURE__ */ new Map();
25670
+ pendingMcpApprovals = /* @__PURE__ */ new Map();
25671
+ handleNotification(notification) {
25672
+ switch (notification.method) {
25673
+ case "item/started":
25674
+ this.handleItemStarted(notification.params.threadId, notification.params.item);
25675
+ return;
25676
+ case "item/completed":
25677
+ this.handleItemCompleted(notification.params.threadId, notification.params.item);
25678
+ return;
25679
+ case "turn/completed":
25680
+ this.clearTransientState(notification.params.threadId);
25681
+ return;
25682
+ case "serverRequest/resolved":
25683
+ this.pendingMcpApprovals.delete(notification.params.threadId);
25684
+ return;
25685
+ default:
25686
+ return;
25069
25687
  }
25070
- return `Allow Commands Starting With \`${commandPrefix}\``;
25071
25688
  }
25072
- networkPolicyAmendmentLabel(amendment) {
25073
- return amendment.action === "allow" ? `Allow ${amendment.host} in the Future` : `Block ${amendment.host} in the Future`;
25689
+ fileChange(threadId, itemId) {
25690
+ return this.fileChanges.get(threadId)?.get(itemId);
25074
25691
  }
25075
- formatRequestedPermissions(permissions) {
25076
- const content = [];
25077
- if (permissions.network?.enabled !== void 0 && permissions.network.enabled !== null) {
25078
- content.push(`Network Access: ${permissions.network.enabled}`);
25079
- }
25080
- if (permissions.fileSystem?.read?.length) {
25081
- content.push(`File System Read Access: ${permissions.fileSystem.read.join(", ")}`);
25082
- }
25083
- if (permissions.fileSystem?.write?.length) {
25084
- content.push(`File System Write Access: ${permissions.fileSystem.write.join(", ")}`);
25692
+ popPendingMcpApproval(threadId, serverName) {
25693
+ const byServer = this.pendingMcpApprovals.get(threadId);
25694
+ if (!byServer) return void 0;
25695
+ const pending = byServer.get(serverName);
25696
+ if (pending?.length !== 1) return void 0;
25697
+ const callId = pending[0];
25698
+ byServer.delete(serverName);
25699
+ if (byServer.size === 0) this.pendingMcpApprovals.delete(threadId);
25700
+ return callId;
25701
+ }
25702
+ nextStandaloneMcpToolCallId(serverName) {
25703
+ return this.nextStandaloneId(serverName);
25704
+ }
25705
+ handleItemStarted(threadId, item) {
25706
+ if (item.type === "fileChange") {
25707
+ const byItem = this.fileChanges.get(threadId) ?? /* @__PURE__ */ new Map();
25708
+ byItem.set(item.id, item);
25709
+ this.fileChanges.set(threadId, byItem);
25710
+ return;
25085
25711
  }
25086
- if (permissions.fileSystem?.entries?.length) {
25087
- content.push(`File System Entries: ${JSON.stringify(permissions.fileSystem.entries)}`);
25712
+ if (item.type !== "mcpToolCall") return;
25713
+ const byServer = this.pendingMcpApprovals.get(threadId) ?? /* @__PURE__ */ new Map();
25714
+ const pending = byServer.get(item.server);
25715
+ if (pending) pending.push(item.id);
25716
+ else byServer.set(item.server, [item.id]);
25717
+ this.pendingMcpApprovals.set(threadId, byServer);
25718
+ }
25719
+ handleItemCompleted(threadId, item) {
25720
+ if (item.type === "fileChange") {
25721
+ const byItem = this.fileChanges.get(threadId);
25722
+ byItem?.delete(item.id);
25723
+ if (byItem?.size === 0) this.fileChanges.delete(threadId);
25724
+ return;
25088
25725
  }
25089
- return content.length > 0 ? content.join("\n\n") : null;
25726
+ if (item.type !== "mcpToolCall") return;
25727
+ const byServer = this.pendingMcpApprovals.get(threadId);
25728
+ if (!byServer) return;
25729
+ const pending = byServer.get(item.server);
25730
+ if (!pending) return;
25731
+ const index = pending.indexOf(item.id);
25732
+ if (index >= 0) pending.splice(index, 1);
25733
+ if (pending.length === 0) byServer.delete(item.server);
25734
+ if (byServer.size === 0) this.pendingMcpApprovals.delete(threadId);
25090
25735
  }
25091
- createContent(lines) {
25092
- const text = lines.filter((line) => !!line).join("\n\n");
25093
- if (!text) return void 0;
25094
- return [{
25095
- type: "content",
25096
- content: {
25097
- type: "text",
25098
- text
25099
- }
25100
- }];
25736
+ clearTransientState(threadId) {
25737
+ this.fileChanges.delete(threadId);
25738
+ this.pendingMcpApprovals.delete(threadId);
25101
25739
  }
25102
25740
  };
25103
25741
 
25104
- // src/McpApprovalOptionId.ts
25105
- var McpApprovalOptionId = {
25106
- AllowOnce: "allow_once",
25107
- AllowSession: "allow_session",
25108
- AllowAlways: "allow_always",
25109
- Decline: "decline"
25110
- };
25742
+ // src/permissions/plan-review.ts
25743
+ var IMPLEMENT_PLAN_OPTION_ID = "implement_plan";
25744
+ var REVISE_PLAN_OPTION_ID = "revise_plan";
25745
+ function planImplementationPermissionRequest(sessionId, plan) {
25746
+ return {
25747
+ sessionId,
25748
+ toolCall: {
25749
+ toolCallId: planImplementationToolCallId(plan),
25750
+ title: "Implement this plan?",
25751
+ kind: "switch_mode",
25752
+ status: "pending",
25753
+ rawInput: { plan: plan.text }
25754
+ },
25755
+ options: [
25756
+ { optionId: IMPLEMENT_PLAN_OPTION_ID, name: "Yes, implement this plan", kind: "allow_once" },
25757
+ {
25758
+ optionId: REVISE_PLAN_OPTION_ID,
25759
+ name: "No, and tell Codex what to do differently",
25760
+ kind: "reject_once"
25761
+ }
25762
+ ],
25763
+ _meta: { codex: { kind: "plan_review", planItemId: plan.itemId } }
25764
+ };
25765
+ }
25766
+ function planImplementationApproved(response) {
25767
+ return response.outcome.outcome === "selected" && response.outcome.optionId === IMPLEMENT_PLAN_OPTION_ID;
25768
+ }
25769
+ function planImplementationToolCallId(plan) {
25770
+ return `plan-review:${plan.itemId}`;
25771
+ }
25111
25772
 
25112
25773
  // src/ElicitationCapabilities.ts
25113
25774
  function clientSupportsFormElicitation(clientCapabilities) {
@@ -25117,57 +25778,146 @@ function clientSupportsUrlElicitation(clientCapabilities) {
25117
25778
  return clientCapabilities?.elicitation?.url != null;
25118
25779
  }
25119
25780
 
25120
- // src/CodexElicitationHandler.ts
25121
- var ELICITATION_OPTIONS = [
25122
- { optionId: "accept", name: "Accept", kind: "allow_once" },
25123
- { optionId: "decline", name: "Decline", kind: "reject_once" }
25124
- ];
25125
- var USER_INPUT_OTHER_FIELD_SUFFIX = "__other";
25781
+ // src/permissions/mcp.ts
25126
25782
  function parsePersistOptions(meta3) {
25127
25783
  const result = /* @__PURE__ */ new Set();
25128
- if (!meta3 || typeof meta3 !== "object") return result;
25784
+ if (!isRecord2(meta3)) return result;
25129
25785
  const persist = meta3["persist"];
25130
- if (persist === "session") {
25131
- result.add("session");
25132
- } else if (persist === "always") {
25133
- result.add("always");
25134
- } else if (Array.isArray(persist)) {
25786
+ if (persist === "session") result.add("session");
25787
+ else if (persist === "always") result.add("always");
25788
+ else if (Array.isArray(persist)) {
25135
25789
  if (persist.includes("session")) result.add("session");
25136
25790
  if (persist.includes("always")) result.add("always");
25137
25791
  }
25138
25792
  return result;
25139
25793
  }
25140
25794
  function isMcpToolCallApproval(meta3) {
25141
- return meta3 !== null && typeof meta3 === "object" && meta3["codex_approval_kind"] === "mcp_tool_call";
25142
- }
25143
- function isRecord2(value) {
25144
- return value !== null && typeof value === "object" && !Array.isArray(value);
25145
- }
25146
- function normalizeJsonValue(value) {
25147
- if (value === null || value === void 0) {
25148
- return null;
25795
+ return isRecord2(meta3) && meta3["codex_approval_kind"] === "mcp_tool_call";
25796
+ }
25797
+ function buildMcpPermissionOptions(isToolApproval, persistOptions) {
25798
+ const options = [permissionOption2(
25799
+ isToolApproval ? McpApprovalOptionId.AllowOnce : "accept",
25800
+ "Allow",
25801
+ "allow_once",
25802
+ isToolApproval ? "Run the tool and continue." : "Allow this request and continue."
25803
+ )];
25804
+ if (persistOptions.has("session")) {
25805
+ options.push(permissionOption2(
25806
+ McpApprovalOptionId.AllowSession,
25807
+ "Allow for this session",
25808
+ "allow_always",
25809
+ isToolApproval ? "Run the tool and remember this choice for this session." : "Allow this request and remember this choice for this session."
25810
+ ));
25149
25811
  }
25150
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
25151
- return value;
25812
+ if (persistOptions.has("always")) {
25813
+ options.push(permissionOption2(
25814
+ McpApprovalOptionId.AllowAlways,
25815
+ "Always allow",
25816
+ "allow_always",
25817
+ isToolApproval ? "Run the tool and remember this choice for future tool calls." : "Allow this request and remember this choice for future requests."
25818
+ ));
25152
25819
  }
25153
- if (typeof value === "bigint") {
25154
- return Number(value);
25820
+ if (isToolApproval) {
25821
+ options.push(permissionOption2(
25822
+ McpApprovalOptionId.Cancel,
25823
+ "Cancel",
25824
+ "reject_once",
25825
+ "Cancel this tool call"
25826
+ ));
25827
+ } else {
25828
+ options.push(
25829
+ permissionOption2(
25830
+ McpApprovalOptionId.Decline,
25831
+ "Deny",
25832
+ "reject_once",
25833
+ "Decline this request and continue."
25834
+ ),
25835
+ permissionOption2(McpApprovalOptionId.Cancel, "Cancel", "reject_once", "Cancel this request")
25836
+ );
25155
25837
  }
25156
- if (Array.isArray(value)) {
25157
- return value.map(normalizeJsonValue);
25838
+ return options;
25839
+ }
25840
+ function buildMcpPermissionRequest(sessionId, params, context, nextStandaloneToolCallId) {
25841
+ const messageContent = {
25842
+ type: "content",
25843
+ content: { type: "text", text: params.message }
25844
+ };
25845
+ const options = buildMcpPermissionOptions(context.isToolApproval, context.persistOptions);
25846
+ if (params.mode === "form" || params.mode === "openai/form") {
25847
+ if (context.correlatedCallId !== void 0) {
25848
+ return {
25849
+ request: {
25850
+ sessionId,
25851
+ toolCall: {
25852
+ toolCallId: context.correlatedCallId,
25853
+ kind: "execute",
25854
+ status: "pending"
25855
+ },
25856
+ _meta: { is_mcp_tool_approval: true },
25857
+ options
25858
+ },
25859
+ correlatedCallId: context.correlatedCallId
25860
+ };
25861
+ }
25862
+ return {
25863
+ request: {
25864
+ sessionId,
25865
+ toolCall: {
25866
+ toolCallId: nextStandaloneToolCallId(),
25867
+ kind: context.isToolApproval ? "execute" : "other",
25868
+ status: "pending",
25869
+ content: [messageContent],
25870
+ rawInput: { serverName: params.serverName, schema: params.requestedSchema }
25871
+ },
25872
+ ...context.isToolApproval ? { _meta: { is_mcp_tool_approval: true } } : {},
25873
+ options
25874
+ },
25875
+ correlatedCallId: void 0
25876
+ };
25158
25877
  }
25159
- if (typeof value === "object") {
25160
- return Object.fromEntries(
25161
- Object.entries(value).filter(([, nested]) => nested !== void 0).map(([key, nested]) => [key, normalizeJsonValue(nested)])
25162
- );
25878
+ return {
25879
+ request: {
25880
+ sessionId,
25881
+ toolCall: {
25882
+ toolCallId: `elicitation-${params.elicitationId}`,
25883
+ kind: "fetch",
25884
+ status: "pending",
25885
+ content: [messageContent],
25886
+ rawInput: { serverName: params.serverName, url: params.url }
25887
+ },
25888
+ options
25889
+ },
25890
+ correlatedCallId: void 0
25891
+ };
25892
+ }
25893
+ function convertMcpPermissionResponse(response, isToolApproval, persistOptions) {
25894
+ if (response.outcome.outcome === "cancelled") return cancelledResponse();
25895
+ switch (response.outcome.optionId) {
25896
+ case McpApprovalOptionId.AllowSession:
25897
+ return persistOptions.has("session") ? { action: "accept", content: null, _meta: { persist: "session" } } : cancelledResponse();
25898
+ case McpApprovalOptionId.AllowAlways:
25899
+ return persistOptions.has("always") ? { action: "accept", content: null, _meta: { persist: "always" } } : cancelledResponse();
25900
+ case McpApprovalOptionId.AllowOnce:
25901
+ return isToolApproval ? { action: "accept", content: null, _meta: null } : cancelledResponse();
25902
+ case "accept":
25903
+ return !isToolApproval ? { action: "accept", content: null, _meta: null } : cancelledResponse();
25904
+ case McpApprovalOptionId.Decline:
25905
+ return !isToolApproval ? { action: "decline", content: null, _meta: null } : cancelledResponse();
25906
+ case McpApprovalOptionId.Cancel:
25907
+ default:
25908
+ return cancelledResponse();
25163
25909
  }
25164
- return String(value);
25165
25910
  }
25166
- function normalizeJsonObject(value) {
25167
- return Object.fromEntries(
25168
- Object.entries(value).filter(([, nested]) => nested !== void 0).map(([key, nested]) => [key, normalizeJsonValue(nested)])
25169
- );
25911
+ function permissionOption2(optionId, name, kind, description) {
25912
+ const meta3 = optionPermissionMeta(description);
25913
+ return { optionId, name, kind, ...meta3 ? { _meta: meta3 } : {} };
25914
+ }
25915
+ function cancelledResponse() {
25916
+ return { action: "cancel", content: null, _meta: null };
25170
25917
  }
25918
+
25919
+ // src/CodexElicitationHandler.ts
25920
+ var USER_INPUT_OTHER_FIELD_SUFFIX = "__other";
25171
25921
  function normalizeElicitationSchema(value) {
25172
25922
  const normalized = normalizeElicitationSchemaValue(value);
25173
25923
  if (!isRecord2(normalized)) {
@@ -25203,40 +25953,6 @@ function normalizeElicitationSchemaValue(value) {
25203
25953
  }
25204
25954
  return result;
25205
25955
  }
25206
- function metaRecord(meta3) {
25207
- return isRecord2(meta3) ? meta3 : null;
25208
- }
25209
- function persistChoiceOption(value) {
25210
- switch (value) {
25211
- case "once":
25212
- return { const: "once", title: "Allow once" };
25213
- case "session":
25214
- return { const: "session", title: "Allow for this session" };
25215
- case "always":
25216
- return { const: "always", title: "Allow and don't ask again" };
25217
- }
25218
- }
25219
- function addPersistChoiceToSchema(schema, persistOptions) {
25220
- if (persistOptions.size === 0) {
25221
- return schema;
25222
- }
25223
- const choices = ["once"];
25224
- if (persistOptions.has("session")) choices.push("session");
25225
- if (persistOptions.has("always")) choices.push("always");
25226
- return {
25227
- ...schema,
25228
- properties: {
25229
- ...schema.properties,
25230
- persist: {
25231
- type: "string",
25232
- title: "Approval scope",
25233
- oneOf: choices.map(persistChoiceOption),
25234
- default: "once"
25235
- }
25236
- },
25237
- required: Array.from(/* @__PURE__ */ new Set([...schema.required ?? [], "persist"]))
25238
- };
25239
- }
25240
25956
  function contentRecord(content) {
25241
25957
  return isRecord2(content) ? content : {};
25242
25958
  }
@@ -25248,7 +25964,7 @@ function jsonObjectOrNull(content) {
25248
25964
  return Object.fromEntries(entries.map(([key, value]) => [key, normalizeJsonValue(value)]));
25249
25965
  }
25250
25966
  function elicitationResponseMeta(response, context, persist = void 0) {
25251
- const responseMeta = metaRecord(response._meta);
25967
+ const responseMeta = recordOrNull(response._meta);
25252
25968
  const meta3 = responseMeta ? normalizeJsonObject(responseMeta) : {};
25253
25969
  if (context.isToolApproval) {
25254
25970
  delete meta3["persist"];
@@ -25279,22 +25995,9 @@ function userInputResponseValue(content, fieldId) {
25279
25995
  }
25280
25996
  return value;
25281
25997
  }
25282
- function buildToolApprovalOptions(persistOptions) {
25283
- const options = [
25284
- { optionId: McpApprovalOptionId.AllowOnce, name: "Allow", kind: "allow_once" }
25285
- ];
25286
- if (persistOptions.has("session")) {
25287
- options.push({ optionId: McpApprovalOptionId.AllowSession, name: "Allow for This Session", kind: "allow_always" });
25288
- }
25289
- if (persistOptions.has("always")) {
25290
- options.push({ optionId: McpApprovalOptionId.AllowAlways, name: "Allow and Don't Ask Again", kind: "allow_always" });
25291
- }
25292
- options.push({ optionId: McpApprovalOptionId.Decline, name: "Decline", kind: "reject_once" });
25293
- return options;
25294
- }
25295
25998
  var CodexElicitationHandler = class {
25296
25999
  connection;
25297
- sessionState;
26000
+ permissionContext;
25298
26001
  clientCapabilities;
25299
26002
  cancellationSignal;
25300
26003
  // In Rust, the MCP elicitation handler receives ElicitationRequestEvent directly from the MCP
@@ -25306,32 +26009,21 @@ var CodexElicitationHandler = class {
25306
26009
  // reaches the client.
25307
26010
  //
25308
26011
  // Workaround: before requesting approval, Codex emits an item/started notification with an
25309
- // mcpToolCall item carrying the call id and server name. We store (threadId, serverName) → callId
25310
- // here so the elicitation request can correlate back to the already-rendered tool call item.
26012
+ // mcpToolCall item carrying the call id and server name. The shared permission lifecycle stores
26013
+ // (threadId, serverName) → callId so this request can correlate to the rendered tool call item.
25311
26014
  //
25312
- // Multiple calls are safe because Codex requests approval synchronously — it blocks on one tool
25313
- // call's elicitation before starting the next, so there is at most one pending approval per
25314
- // (threadId, serverName).
25315
- pendingMcpApprovals = /* @__PURE__ */ new Map();
25316
26015
  // The app-server handler exposes URL elicitationId, while serverRequest/resolved only exposes
25317
26016
  // threadId here, so accepted URL elicitations are completed at thread scope.
25318
26017
  pendingUrlElicitations = /* @__PURE__ */ new Map();
25319
- constructor(connection, sessionState, clientCapabilities = null, cancellationSignal) {
26018
+ constructor(connection, permissionContext, clientCapabilities = null, cancellationSignal) {
25320
26019
  this.connection = connection;
25321
- this.sessionState = sessionState;
26020
+ this.permissionContext = permissionContext;
25322
26021
  this.clientCapabilities = clientCapabilities;
25323
26022
  this.cancellationSignal = cancellationSignal;
25324
26023
  }
25325
26024
  async handleNotification(notification) {
25326
26025
  switch (notification.method) {
25327
- case "item/started":
25328
- this.handleItemStarted(notification.params);
25329
- return;
25330
- case "item/completed":
25331
- this.handleItemCompleted(notification.params);
25332
- return;
25333
26026
  case "serverRequest/resolved":
25334
- this.clearThread(notification.params.threadId);
25335
26027
  await this.completeUrlElicitations(notification.params.threadId);
25336
26028
  return;
25337
26029
  default:
@@ -25347,29 +26039,39 @@ var CodexElicitationHandler = class {
25347
26039
  this.buildElicitationRequest(params, context),
25348
26040
  this.requestOptions()
25349
26041
  );
25350
- const result = this.convertElicitationResponse(response2, context);
25351
- if (params.mode === "url" && result.action === "accept") {
26042
+ const result2 = this.convertElicitationResponse(response2, context);
26043
+ if (params.mode === "url" && result2.action === "accept") {
25352
26044
  this.trackUrlElicitation(params.threadId, params.elicitationId);
25353
26045
  }
25354
- await this.publishAcceptedMcpToolApproval(context, result.action === "accept");
25355
- return result;
26046
+ await this.publishAcceptedMcpToolApproval(params.threadId, context, result2.action === "accept");
26047
+ return result2;
25356
26048
  }
25357
- const { request, correlatedCallId } = this.buildPermissionRequest(params, context);
26049
+ if (!this.canUsePermissionFallback(params)) {
26050
+ return { action: "cancel", content: null, _meta: null };
26051
+ }
26052
+ const { request, correlatedCallId } = buildMcpPermissionRequest(
26053
+ params.threadId,
26054
+ params,
26055
+ context,
26056
+ () => this.permissionContext.nextStandaloneMcpToolCallId(params.serverName)
26057
+ );
25358
26058
  const response = await this.connection.request(
25359
26059
  methods.client.session.requestPermission,
25360
26060
  request,
25361
26061
  this.requestOptions()
25362
26062
  );
25363
- if (correlatedCallId !== void 0 && response.outcome.outcome !== "cancelled") {
25364
- const optionId = response.outcome.optionId;
25365
- if (optionId !== McpApprovalOptionId.Decline) {
25366
- await this.connection.notify(methods.client.session.update, {
25367
- sessionId: this.sessionState.sessionId,
25368
- update: { sessionUpdate: "tool_call_update", toolCallId: correlatedCallId, status: "in_progress" }
25369
- });
25370
- }
26063
+ const result = convertMcpPermissionResponse(
26064
+ response,
26065
+ context.isToolApproval,
26066
+ context.persistOptions
26067
+ );
26068
+ if (correlatedCallId !== void 0 && result.action === "accept") {
26069
+ await this.connection.notify(methods.client.session.update, {
26070
+ sessionId: params.threadId,
26071
+ update: { sessionUpdate: "tool_call_update", toolCallId: correlatedCallId, status: "in_progress" }
26072
+ });
25371
26073
  }
25372
- return this.convertPermissionResponse(response);
26074
+ return result;
25373
26075
  } catch (error51) {
25374
26076
  logger.error("Error handling MCP elicitation request", error51);
25375
26077
  return { action: "cancel", content: null, _meta: null };
@@ -25439,12 +26141,17 @@ var CodexElicitationHandler = class {
25439
26141
  }
25440
26142
  }
25441
26143
  createMcpElicitationContext(params) {
25442
- const isToolApproval = isMcpToolCallApproval(params._meta);
26144
+ const isToolApproval = isMcpToolCallApproval(params._meta) && this.isMessageOnlyForm(params);
25443
26145
  const persistOptions = parsePersistOptions(params._meta);
25444
- const correlatedCallId = isToolApproval && (params.mode === "form" || params.mode === "openai/form") ? this.popPendingApproval(params.threadId, params.serverName) : void 0;
25445
- return { isToolApproval, persistOptions, correlatedCallId };
26146
+ const correlatedCallId = isToolApproval ? this.permissionContext.popPendingMcpApproval(params.threadId, params.serverName) : void 0;
26147
+ return {
26148
+ isToolApproval,
26149
+ persistOptions,
26150
+ correlatedCallId
26151
+ };
25446
26152
  }
25447
26153
  shouldUseAcpElicitation(params) {
26154
+ if (this.isMessageOnlyForm(params)) return false;
25448
26155
  switch (params.mode) {
25449
26156
  case "form":
25450
26157
  return clientSupportsFormElicitation(this.clientCapabilities);
@@ -25454,23 +26161,28 @@ var CodexElicitationHandler = class {
25454
26161
  return false;
25455
26162
  }
25456
26163
  }
26164
+ canUsePermissionFallback(params) {
26165
+ return params.mode === "url" || this.isMessageOnlyForm(params);
26166
+ }
26167
+ isMessageOnlyForm(params) {
26168
+ if (params.mode !== "form" && params.mode !== "openai/form") return false;
26169
+ if (params.requestedSchema === null) return true;
26170
+ if (!isRecord2(params.requestedSchema)) return false;
26171
+ return params.requestedSchema["type"] === "object" && isRecord2(params.requestedSchema["properties"]) && Object.keys(params.requestedSchema["properties"]).length === 0;
26172
+ }
25457
26173
  buildElicitationRequest(params, context) {
25458
26174
  const base = {
25459
- sessionId: this.sessionState.sessionId,
26175
+ sessionId: params.threadId,
25460
26176
  ...context.correlatedCallId ? { toolCallId: context.correlatedCallId } : {},
25461
26177
  message: params.message,
25462
- _meta: metaRecord(params._meta)
26178
+ _meta: recordOrNull(params._meta)
25463
26179
  };
25464
26180
  switch (params.mode) {
25465
26181
  case "form": {
25466
- const requestedSchema = context.isToolApproval ? addPersistChoiceToSchema(
25467
- normalizeElicitationSchema(params.requestedSchema),
25468
- context.persistOptions
25469
- ) : normalizeElicitationSchema(params.requestedSchema);
25470
26182
  return {
25471
26183
  ...base,
25472
26184
  mode: "form",
25473
- requestedSchema
26185
+ requestedSchema: normalizeElicitationSchema(params.requestedSchema)
25474
26186
  };
25475
26187
  }
25476
26188
  case "url":
@@ -25526,105 +26238,35 @@ var CodexElicitationHandler = class {
25526
26238
  isOtherAnswer: true,
25527
26239
  isSecret: question.isSecret
25528
26240
  }
25529
- }
25530
- };
25531
- }
25532
- }
25533
- const firstQuestion = params.questions[0];
25534
- return {
25535
- sessionId: this.sessionState.sessionId,
25536
- toolCallId: params.itemId,
25537
- mode: "form",
25538
- message: params.questions.length === 1 && firstQuestion ? firstQuestion.question : "Input requested",
25539
- requestedSchema: {
25540
- type: "object",
25541
- properties,
25542
- required: required2
25543
- },
25544
- _meta: {
25545
- codex: {
25546
- autoResolutionMs: params.autoResolutionMs
25547
- }
25548
- }
25549
- };
25550
- }
25551
- buildPermissionRequest(params, context) {
25552
- const sessionId = this.sessionState.sessionId;
25553
- const messageContent = {
25554
- type: "content",
25555
- content: { type: "text", text: params.message }
25556
- };
25557
- const options = context.isToolApproval ? buildToolApprovalOptions(context.persistOptions) : ELICITATION_OPTIONS;
25558
- if (params.mode === "form" || params.mode === "openai/form") {
25559
- if (context.correlatedCallId !== void 0) {
25560
- return {
25561
- request: {
25562
- sessionId,
25563
- toolCall: {
25564
- toolCallId: context.correlatedCallId,
25565
- kind: "execute",
25566
- status: "pending"
25567
- // content: [messageContent], — omitted: already rendered via item/started
25568
- // rawInput: { ... } — omitted: same reason
25569
- },
25570
- _meta: { is_mcp_tool_approval: true },
25571
- options
25572
- },
25573
- correlatedCallId: context.correlatedCallId
25574
- };
25575
- }
25576
- return {
25577
- request: {
25578
- sessionId,
25579
- toolCall: {
25580
- toolCallId: `elicitation-${params.serverName}`,
25581
- kind: context.isToolApproval ? "execute" : "other",
25582
- status: "pending",
25583
- content: [messageContent],
25584
- rawInput: { serverName: params.serverName, schema: params.requestedSchema }
25585
- },
25586
- ...context.isToolApproval ? { _meta: { is_mcp_tool_approval: true } } : {},
25587
- options
25588
- },
25589
- correlatedCallId: void 0
25590
- };
25591
- } else {
25592
- return {
25593
- request: {
25594
- sessionId,
25595
- toolCall: {
25596
- toolCallId: `elicitation-${params.elicitationId}`,
25597
- kind: "fetch",
25598
- status: "pending",
25599
- content: [messageContent],
25600
- rawInput: { serverName: params.serverName, url: params.url }
25601
- },
25602
- options
25603
- },
25604
- correlatedCallId: void 0
25605
- };
25606
- }
25607
- }
25608
- convertPermissionResponse(response) {
25609
- if (response.outcome.outcome === "cancelled") {
25610
- return { action: "cancel", content: null, _meta: null };
25611
- }
25612
- const optionId = response.outcome.optionId;
25613
- if (optionId === McpApprovalOptionId.AllowSession) {
25614
- return { action: "accept", content: null, _meta: { persist: "session" } };
25615
- }
25616
- if (optionId === McpApprovalOptionId.AllowAlways) {
25617
- return { action: "accept", content: null, _meta: { persist: "always" } };
25618
- }
25619
- if (optionId === McpApprovalOptionId.AllowOnce || optionId === "accept") {
25620
- return { action: "accept", content: null, _meta: null };
26241
+ }
26242
+ };
26243
+ }
25621
26244
  }
25622
- return { action: "decline", content: null, _meta: null };
26245
+ const firstQuestion = params.questions[0];
26246
+ return {
26247
+ sessionId: params.threadId,
26248
+ toolCallId: params.itemId,
26249
+ mode: "form",
26250
+ message: params.questions.length === 1 && firstQuestion ? firstQuestion.question : "Input requested",
26251
+ requestedSchema: {
26252
+ type: "object",
26253
+ properties,
26254
+ required: required2
26255
+ },
26256
+ _meta: {
26257
+ codex: {
26258
+ autoResolutionMs: params.autoResolutionMs
26259
+ }
26260
+ }
26261
+ };
25623
26262
  }
25624
26263
  convertElicitationResponse(response, context) {
25625
26264
  if (CreateElicitationResponse.isAccept(response)) {
25626
26265
  const content = contentRecord(response.content);
25627
26266
  const persist = context.isToolApproval ? content["persist"] : void 0;
26267
+ if (context.isToolApproval && !this.isAllowedToolApprovalPersist(persist, context.persistOptions)) {
26268
+ return { action: "cancel", content: null, _meta: null };
26269
+ }
25628
26270
  if (persist === "session" || persist === "always" || persist === "once") {
25629
26271
  delete content["persist"];
25630
26272
  }
@@ -25635,7 +26277,7 @@ var CodexElicitationHandler = class {
25635
26277
  };
25636
26278
  }
25637
26279
  if (CreateElicitationResponse.isDecline(response)) {
25638
- return { action: "decline", content: null, _meta: elicitationResponseMeta(response, context) };
26280
+ return context.isToolApproval ? { action: "cancel", content: null, _meta: elicitationResponseMeta(response, context) } : { action: "decline", content: null, _meta: elicitationResponseMeta(response, context) };
25639
26281
  }
25640
26282
  if (CreateElicitationResponse.isCancel(response)) {
25641
26283
  return { action: "cancel", content: null, _meta: elicitationResponseMeta(response, context) };
@@ -25645,6 +26287,9 @@ var CodexElicitationHandler = class {
25645
26287
  }
25646
26288
  return { action: "cancel", content: null, _meta: null };
25647
26289
  }
26290
+ isAllowedToolApprovalPersist(persist, persistOptions) {
26291
+ return persist === void 0 || persist === "once" || persist === "session" && persistOptions.has("session") || persist === "always" && persistOptions.has("always");
26292
+ }
25648
26293
  convertUserInputResponse(response, params) {
25649
26294
  if (!CreateElicitationResponse.isAccept(response)) {
25650
26295
  return { answers: {} };
@@ -25663,12 +26308,12 @@ var CodexElicitationHandler = class {
25663
26308
  }
25664
26309
  return { answers };
25665
26310
  }
25666
- async publishAcceptedMcpToolApproval(context, accepted) {
26311
+ async publishAcceptedMcpToolApproval(sessionId, context, accepted) {
25667
26312
  if (!accepted || context.correlatedCallId === void 0) {
25668
26313
  return;
25669
26314
  }
25670
26315
  await this.connection.notify(methods.client.session.update, {
25671
- sessionId: this.sessionState.sessionId,
26316
+ sessionId,
25672
26317
  update: { sessionUpdate: "tool_call_update", toolCallId: context.correlatedCallId, status: "in_progress" }
25673
26318
  });
25674
26319
  }
@@ -25692,37 +26337,6 @@ var CodexElicitationHandler = class {
25692
26337
  });
25693
26338
  }
25694
26339
  }
25695
- handleItemStarted(event) {
25696
- if (event.item.type !== "mcpToolCall") {
25697
- return;
25698
- }
25699
- this.pendingMcpApprovals.set(this.key(event.threadId, event.item.server), event.item.id);
25700
- }
25701
- handleItemCompleted(event) {
25702
- if (event.item.type !== "mcpToolCall") {
25703
- return;
25704
- }
25705
- this.popPendingApproval(event.threadId, event.item.server);
25706
- }
25707
- popPendingApproval(threadId, serverName) {
25708
- const key = this.key(threadId, serverName);
25709
- const callId = this.pendingMcpApprovals.get(key);
25710
- this.pendingMcpApprovals.delete(key);
25711
- return callId;
25712
- }
25713
- clearThread(threadId) {
25714
- for (const key of this.pendingMcpApprovals.keys()) {
25715
- if (this.belongsToThread(key, threadId)) {
25716
- this.pendingMcpApprovals.delete(key);
25717
- }
25718
- }
25719
- }
25720
- key(threadId, serverName) {
25721
- return `${threadId}:${serverName}`;
25722
- }
25723
- belongsToThread(key, threadId) {
25724
- return key.startsWith(`${threadId}:`);
25725
- }
25726
26340
  };
25727
26341
 
25728
26342
  // src/CodexAuthMethod.ts
@@ -25778,13 +26392,14 @@ function isCodexAuthRequest(request) {
25778
26392
  }
25779
26393
 
25780
26394
  // node_modules/open/index.js
25781
- import process9 from "node:process";
25782
- import path3 from "node:path";
26395
+ import process11 from "node:process";
26396
+ import path4 from "node:path";
25783
26397
  import { fileURLToPath } from "node:url";
25784
- import childProcess3 from "node:child_process";
26398
+ import childProcess4 from "node:child_process";
25785
26399
  import fs6, { constants as fsConstants2 } from "node:fs/promises";
25786
26400
 
25787
26401
  // node_modules/wsl-utils/index.js
26402
+ import path3 from "node:path";
25788
26403
  import { promisify as promisify2 } from "node:util";
25789
26404
  import childProcess2 from "node:child_process";
25790
26405
  import fs5, { constants as fsConstants } from "node:fs/promises";
@@ -25863,7 +26478,7 @@ var isWsl = () => {
25863
26478
  };
25864
26479
  var is_wsl_default = process3.env.__IS_WSL_TEST__ ? isWsl : isWsl();
25865
26480
 
25866
- // node_modules/powershell-utils/index.js
26481
+ // node_modules/wsl-utils/node_modules/powershell-utils/index.js
25867
26482
  import process4 from "node:process";
25868
26483
  import { Buffer as Buffer2 } from "node:buffer";
25869
26484
  import { promisify } from "node:util";
@@ -25962,20 +26577,73 @@ var canAccessPowerShell = async () => {
25962
26577
  var wslDefaultBrowser = async () => {
25963
26578
  const psPath = await powerShellPath2();
25964
26579
  const command = String.raw`(Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice").ProgId`;
25965
- const { stdout } = await executePowerShell(command, { powerShellPath: psPath });
26580
+ const { stdout } = await executePowerShell(command, {
26581
+ powerShellPath: psPath,
26582
+ cwd: path3.dirname(psPath)
26583
+ });
25966
26584
  return stdout.trim();
25967
26585
  };
25968
- var convertWslPathToWindows = async (path8) => {
25969
- if (/^[a-z]+:\/\//i.test(path8)) {
25970
- return path8;
25971
- }
25972
- try {
25973
- const { stdout } = await execFile2("wslpath", ["-aw", path8], { encoding: "utf8" });
25974
- return stdout.trim();
25975
- } catch {
25976
- return path8;
26586
+ var isUrl = (path9) => /^[a-z]+:\/\//i.test(path9);
26587
+ var convertWslPathToWindows = async (paths) => {
26588
+ const isBatch = Array.isArray(paths);
26589
+ const pathArray = isBatch ? paths : [paths];
26590
+ const indicesToConvert = [];
26591
+ const pathsToConvert = [];
26592
+ for (const [index, path9] of pathArray.entries()) {
26593
+ if (!isUrl(path9)) {
26594
+ indicesToConvert.push(index);
26595
+ pathsToConvert.push(path9);
26596
+ }
26597
+ }
26598
+ const results = [...pathArray];
26599
+ if (pathsToConvert.length > 0) {
26600
+ try {
26601
+ const { stdout } = await execFile2("wslpath", ["-aw", ...pathsToConvert], { encoding: "utf8" });
26602
+ const convertedPaths = stdout.split(/\r?\n/).filter(Boolean);
26603
+ for (const [index, originalIndex] of indicesToConvert.entries()) {
26604
+ results[originalIndex] = convertedPaths[index] ?? pathArray[originalIndex];
26605
+ }
26606
+ } catch {
26607
+ }
25977
26608
  }
26609
+ return isBatch ? results : results[0];
26610
+ };
26611
+
26612
+ // node_modules/powershell-utils/index.js
26613
+ import process5 from "node:process";
26614
+ import { Buffer as Buffer3 } from "node:buffer";
26615
+ import { promisify as promisify3 } from "node:util";
26616
+ import childProcess3 from "node:child_process";
26617
+ var execFile3 = promisify3(childProcess3.execFile);
26618
+ var powerShellPath3 = () => `${process5.env.SYSTEMROOT || process5.env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
26619
+ var argumentsPrefix = [
26620
+ "-NoProfile",
26621
+ "-NonInteractive",
26622
+ "-ExecutionPolicy",
26623
+ "Bypass",
26624
+ "-EncodedCommand"
26625
+ ];
26626
+ var encodeCommand = (command) => Buffer3.from(command, "utf16le").toString("base64");
26627
+ var escapeArgument = (value) => `'${String(value).replaceAll("'", "''")}'`;
26628
+ var createArguments = (command) => [...argumentsPrefix, encodeCommand(command)];
26629
+ var executePowerShell2 = async (command, options = {}) => {
26630
+ const {
26631
+ powerShellPath: psPath,
26632
+ ...execFileOptions
26633
+ } = options;
26634
+ return execFile3(
26635
+ psPath ?? powerShellPath3(),
26636
+ createArguments(command),
26637
+ {
26638
+ encoding: "utf8",
26639
+ ...execFileOptions
26640
+ }
26641
+ );
25978
26642
  };
26643
+ executePowerShell2.argumentsPrefix = argumentsPrefix;
26644
+ executePowerShell2.encodeCommand = encodeCommand;
26645
+ executePowerShell2.escapeArgument = escapeArgument;
26646
+ executePowerShell2.createArguments = createArguments;
25979
26647
 
25980
26648
  // node_modules/define-lazy-prop/index.js
25981
26649
  function defineLazyProperty(object2, propertyName, valueGetter) {
@@ -25996,17 +26664,17 @@ function defineLazyProperty(object2, propertyName, valueGetter) {
25996
26664
  }
25997
26665
 
25998
26666
  // node_modules/default-browser/index.js
25999
- import { promisify as promisify6 } from "node:util";
26000
- import process7 from "node:process";
26001
- import { execFile as execFile6 } from "node:child_process";
26667
+ import { promisify as promisify7 } from "node:util";
26668
+ import process9 from "node:process";
26669
+ import { execFile as execFile7 } from "node:child_process";
26002
26670
 
26003
26671
  // node_modules/default-browser-id/index.js
26004
- import { promisify as promisify3 } from "node:util";
26005
- import process5 from "node:process";
26006
- import { execFile as execFile3 } from "node:child_process";
26007
- var execFileAsync = promisify3(execFile3);
26672
+ import { promisify as promisify4 } from "node:util";
26673
+ import process6 from "node:process";
26674
+ import { execFile as execFile4 } from "node:child_process";
26675
+ var execFileAsync = promisify4(execFile4);
26008
26676
  async function defaultBrowserId() {
26009
- if (process5.platform !== "darwin") {
26677
+ if (process6.platform !== "darwin") {
26010
26678
  throw new Error("macOS only");
26011
26679
  }
26012
26680
  const { stdout } = await execFileAsync("defaults", ["read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers"]);
@@ -26019,12 +26687,12 @@ async function defaultBrowserId() {
26019
26687
  }
26020
26688
 
26021
26689
  // node_modules/run-applescript/index.js
26022
- import process6 from "node:process";
26023
- import { promisify as promisify4 } from "node:util";
26024
- import { execFile as execFile4, execFileSync } from "node:child_process";
26025
- var execFileAsync2 = promisify4(execFile4);
26690
+ import process7 from "node:process";
26691
+ import { promisify as promisify5 } from "node:util";
26692
+ import { execFile as execFile5, execFileSync } from "node:child_process";
26693
+ var execFileAsync2 = promisify5(execFile5);
26026
26694
  async function runAppleScript(script, { humanReadableOutput = true, signal } = {}) {
26027
- if (process6.platform !== "darwin") {
26695
+ if (process7.platform !== "darwin") {
26028
26696
  throw new Error("macOS only");
26029
26697
  }
26030
26698
  const outputArguments = humanReadableOutput ? [] : ["-ss"];
@@ -26043,9 +26711,10 @@ tell application "System Events" to get value of property list item "CFBundleNam
26043
26711
  }
26044
26712
 
26045
26713
  // node_modules/default-browser/windows.js
26046
- import { promisify as promisify5 } from "node:util";
26047
- import { execFile as execFile5 } from "node:child_process";
26048
- var execFileAsync3 = promisify5(execFile5);
26714
+ import process8 from "node:process";
26715
+ import { promisify as promisify6 } from "node:util";
26716
+ import { execFile as execFile6 } from "node:child_process";
26717
+ var execFileAsync3 = promisify6(execFile6);
26049
26718
  var windowsBrowserProgIds = {
26050
26719
  MSEdgeHTM: { name: "Edge", id: "com.microsoft.edge" },
26051
26720
  // The missing `L` is correct.
@@ -26069,7 +26738,8 @@ var _windowsBrowserProgIdMap = new Map(Object.entries(windowsBrowserProgIds));
26069
26738
  var UnknownBrowserError = class extends Error {
26070
26739
  };
26071
26740
  async function defaultBrowser(_execFileAsync = execFileAsync3) {
26072
- const { stdout } = await _execFileAsync("reg", [
26741
+ const regPath = `${process8.env.SYSTEMROOT ?? process8.env.windir ?? "C:\\Windows"}\\System32\\reg.exe`;
26742
+ const { stdout } = await _execFileAsync(regPath, [
26073
26743
  "QUERY",
26074
26744
  " HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
26075
26745
  "/v",
@@ -26088,36 +26758,36 @@ async function defaultBrowser(_execFileAsync = execFileAsync3) {
26088
26758
  }
26089
26759
 
26090
26760
  // node_modules/default-browser/index.js
26091
- var execFileAsync4 = promisify6(execFile6);
26761
+ var execFileAsync4 = promisify7(execFile7);
26092
26762
  var titleize = (string4) => string4.toLowerCase().replaceAll(/(?:^|\s|-)\S/g, (x) => x.toUpperCase());
26093
26763
  async function defaultBrowser2() {
26094
- if (process7.platform === "darwin") {
26764
+ if (process9.platform === "darwin") {
26095
26765
  const id = await defaultBrowserId();
26096
26766
  const name = await bundleName(id);
26097
26767
  return { name, id };
26098
26768
  }
26099
- if (process7.platform === "linux") {
26769
+ if (process9.platform === "linux") {
26100
26770
  const { stdout } = await execFileAsync4("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
26101
26771
  const id = stdout.trim();
26102
26772
  const name = titleize(id.replace(/.desktop$/, "").replace("-", " "));
26103
26773
  return { name, id };
26104
26774
  }
26105
- if (process7.platform === "win32") {
26775
+ if (process9.platform === "win32") {
26106
26776
  return defaultBrowser();
26107
26777
  }
26108
26778
  throw new Error("Only macOS, Linux, and Windows are supported");
26109
26779
  }
26110
26780
 
26111
26781
  // node_modules/is-in-ssh/index.js
26112
- import process8 from "node:process";
26113
- var isInSsh = Boolean(process8.env.SSH_CONNECTION || process8.env.SSH_CLIENT || process8.env.SSH_TTY);
26782
+ import process10 from "node:process";
26783
+ var isInSsh = Boolean(process10.env.SSH_CONNECTION || process10.env.SSH_CLIENT || process10.env.SSH_TTY);
26114
26784
  var is_in_ssh_default = isInSsh;
26115
26785
 
26116
26786
  // node_modules/open/index.js
26117
26787
  var fallbackAttemptSymbol = /* @__PURE__ */ Symbol("fallbackAttempt");
26118
- var __dirname = import.meta.url ? path3.dirname(fileURLToPath(import.meta.url)) : "";
26119
- var localXdgOpenPath = path3.join(__dirname, "xdg-open");
26120
- var { platform, arch } = process9;
26788
+ var __dirname = import.meta.url ? path4.dirname(fileURLToPath(import.meta.url)) : "";
26789
+ var localXdgOpenPath = path4.join(__dirname, "xdg-open");
26790
+ var { platform, arch } = process11;
26121
26791
  var tryEachApp = async (apps2, opener) => {
26122
26792
  if (apps2.length === 0) {
26123
26793
  return;
@@ -26230,7 +26900,7 @@ var baseOpen = async (options) => {
26230
26900
  }
26231
26901
  } else if (platform === "win32" || shouldUseWindowsInWsl) {
26232
26902
  command = await powerShellPath2();
26233
- cliArguments.push(...executePowerShell.argumentsPrefix);
26903
+ cliArguments.push(...executePowerShell2.argumentsPrefix);
26234
26904
  if (!is_wsl_default) {
26235
26905
  childProcessOptions.windowsVerbatimArguments = true;
26236
26906
  }
@@ -26242,21 +26912,24 @@ var baseOpen = async (options) => {
26242
26912
  encodedArguments.push("-Wait");
26243
26913
  }
26244
26914
  if (app) {
26245
- encodedArguments.push(executePowerShell.escapeArgument(app));
26915
+ encodedArguments.push(executePowerShell2.escapeArgument(app));
26246
26916
  if (options.target) {
26247
26917
  appArguments.push(options.target);
26248
26918
  }
26249
26919
  } else if (options.target) {
26250
- encodedArguments.push(executePowerShell.escapeArgument(options.target));
26920
+ encodedArguments.push(executePowerShell2.escapeArgument(options.target));
26251
26921
  }
26252
26922
  if (appArguments.length > 0) {
26253
- appArguments = appArguments.map((argument) => executePowerShell.escapeArgument(argument));
26923
+ appArguments = appArguments.map((argument) => executePowerShell2.escapeArgument(argument));
26254
26924
  encodedArguments.push("-ArgumentList", appArguments.join(","));
26255
26925
  }
26256
- options.target = executePowerShell.encodeCommand(encodedArguments.join(" "));
26926
+ options.target = executePowerShell2.encodeCommand(encodedArguments.join(" "));
26257
26927
  if (!options.wait) {
26258
26928
  childProcessOptions.stdio = "ignore";
26259
26929
  }
26930
+ if (is_wsl_default) {
26931
+ childProcessOptions.cwd = path4.dirname(command);
26932
+ }
26260
26933
  } else {
26261
26934
  if (app) {
26262
26935
  command = app;
@@ -26268,7 +26941,7 @@ var baseOpen = async (options) => {
26268
26941
  exeLocalXdgOpen = true;
26269
26942
  } catch {
26270
26943
  }
26271
- const useSystemXdgOpen = process9.versions.electron ?? (platform === "android" || isBundled || !exeLocalXdgOpen);
26944
+ const useSystemXdgOpen = process11.versions.electron ?? (platform === "android" || isBundled || !exeLocalXdgOpen);
26272
26945
  command = useSystemXdgOpen ? "xdg-open" : localXdgOpenPath;
26273
26946
  }
26274
26947
  if (appArguments.length > 0) {
@@ -26285,7 +26958,7 @@ var baseOpen = async (options) => {
26285
26958
  if (options.target) {
26286
26959
  cliArguments.push(options.target);
26287
26960
  }
26288
- const subprocess = childProcess3.spawn(command, cliArguments, childProcessOptions);
26961
+ const subprocess = childProcess4.spawn(command, cliArguments, childProcessOptions);
26289
26962
  if (options.wait) {
26290
26963
  return new Promise((resolve, reject) => {
26291
26964
  subprocess.once("error", reject);
@@ -26432,33 +27105,44 @@ var AgentMode = class _AgentMode {
26432
27105
  id;
26433
27106
  name;
26434
27107
  description;
27108
+ kind;
26435
27109
  approvalPolicy;
27110
+ approvalsReviewer;
26436
27111
  sandboxPolicy;
26437
27112
  sandboxMode;
26438
- constructor(id, name, description, approval, sandbox, sandboxMode) {
27113
+ constructor(id, name, description, kind, approvalPolicy, approvalsReviewer, sandboxPolicy, sandboxMode) {
26439
27114
  this.id = id;
26440
27115
  this.name = name;
26441
27116
  this.description = description;
26442
- this.approvalPolicy = approval;
26443
- this.sandboxPolicy = sandbox;
27117
+ this.kind = kind;
27118
+ this.approvalPolicy = approvalPolicy;
27119
+ this.approvalsReviewer = approvalsReviewer;
27120
+ this.sandboxPolicy = sandboxPolicy;
26444
27121
  this.sandboxMode = sandboxMode;
26445
27122
  }
26446
27123
  static ReadOnly = new _AgentMode(
26447
27124
  "read-only",
26448
- "Read-only",
26449
- "Requires approval to edit files and run commands.",
27125
+ "Ask for approval",
27126
+ "Always ask to edit external files and use the internet",
27127
+ "standard",
26450
27128
  "on-request",
27129
+ "user",
26451
27130
  {
26452
- "type": "readOnly",
26453
- "networkAccess": false
27131
+ type: "workspaceWrite",
27132
+ writableRoots: [],
27133
+ networkAccess: false,
27134
+ excludeTmpdirEnvVar: false,
27135
+ excludeSlashTmp: false
26454
27136
  },
26455
- "read-only"
27137
+ "workspace-write"
26456
27138
  );
26457
27139
  static Agent = new _AgentMode(
26458
27140
  "agent",
26459
- "Agent",
26460
- "Read and edit files, and run commands.",
27141
+ "Approve for me",
27142
+ "Only ask for actions detected as potentially unsafe",
27143
+ "auto_review",
26461
27144
  "on-request",
27145
+ "auto_review",
26462
27146
  {
26463
27147
  type: "workspaceWrite",
26464
27148
  writableRoots: [],
@@ -26470,9 +27154,11 @@ var AgentMode = class _AgentMode {
26470
27154
  );
26471
27155
  static AgentFullAccess = new _AgentMode(
26472
27156
  "agent-full-access",
26473
- "Agent (full access)",
26474
- "Codex can edit files outside this workspace and run commands with network access. Exercise caution when using.",
27157
+ "Full access",
27158
+ "Unrestricted access to the internet and any file on your computer",
27159
+ "full_access",
26475
27160
  "never",
27161
+ "user",
26476
27162
  { "type": "dangerFullAccess" },
26477
27163
  "danger-full-access"
26478
27164
  );
@@ -26481,7 +27167,8 @@ var AgentMode = class _AgentMode {
26481
27167
  return {
26482
27168
  id: this.id,
26483
27169
  name: this.name,
26484
- description: this.description
27170
+ description: this.description,
27171
+ _meta: { kind: this.kind }
26485
27172
  };
26486
27173
  }
26487
27174
  toSessionModeState() {
@@ -26501,7 +27188,8 @@ var AgentMode = class _AgentMode {
26501
27188
  options: _AgentMode.all().map((mode) => ({
26502
27189
  value: mode.id,
26503
27190
  name: mode.name,
26504
- description: mode.description
27191
+ description: mode.description,
27192
+ _meta: { kind: mode.kind }
26505
27193
  }))
26506
27194
  };
26507
27195
  }
@@ -26523,7 +27211,7 @@ var AgentMode = class _AgentMode {
26523
27211
  };
26524
27212
 
26525
27213
  // src/CodexAcpClient.ts
26526
- import path6 from "node:path";
27214
+ import path7 from "node:path";
26527
27215
 
26528
27216
  // src/McpServerName.ts
26529
27217
  var MCP_SERVER_NAME_WHITESPACE = /\p{White_Space}/gu;
@@ -26537,7 +27225,7 @@ var package_default = {
26537
27225
  publishConfig: {
26538
27226
  access: "public"
26539
27227
  },
26540
- version: "1.6.0",
27228
+ version: "1.7.0",
26541
27229
  description: "",
26542
27230
  main: "dist/index.js",
26543
27231
  bin: {
@@ -26571,7 +27259,7 @@ var package_default = {
26571
27259
  "example:steering:multistep": "node --import tsx examples/steering.ts",
26572
27260
  "generate-types": "./node_modules/.bin/codex app-server generate-ts --out src/app-server",
26573
27261
  "release:preflight": "bash scripts/release-preflight.sh",
26574
- test: "vitest run",
27262
+ test: "vitest run --no-file-parallelism --retry=2",
26575
27263
  "test:e2e": "npm run build && RUN_E2E_TESTS=true vitest run --no-file-parallelism --retry=2 src/__tests__/CodexACPAgent/e2e",
26576
27264
  "test:watch": "vitest",
26577
27265
  typecheck: "tsc --noEmit && tsc --noEmit -p examples/tsconfig.json",
@@ -26591,17 +27279,17 @@ var package_default = {
26591
27279
  type: "module",
26592
27280
  devDependencies: {
26593
27281
  "@types/node": "^26.1.0",
26594
- esbuild: "^0.28.1",
27282
+ esbuild: "^0.28.2",
26595
27283
  "mcp-hello-world": "^1.1.2",
26596
- tsx: "^4.23.1",
27284
+ tsx: "^4.23.12",
26597
27285
  typescript: "^7.0.2",
26598
- vitest: "^4.1.10"
27286
+ vitest: "^4.1.11"
26599
27287
  },
26600
27288
  dependencies: {
26601
- "@agentclientprotocol/sdk": "^1.3.0",
27289
+ "@agentclientprotocol/sdk": "^1.4.0",
26602
27290
  "@openai/codex": "^0.148.0",
26603
27291
  diff: "^9.0.0",
26604
- open: "^11.0.0",
27292
+ open: "^11.0.1",
26605
27293
  "vscode-jsonrpc": "^9.0.1",
26606
27294
  zod: "^4.0.0"
26607
27295
  }
@@ -26643,17 +27331,17 @@ function createCodexCollaborationMode(mode, currentModelId) {
26643
27331
  }
26644
27332
 
26645
27333
  // src/PathUtils.ts
26646
- import path4 from "node:path";
27334
+ import path5 from "node:path";
26647
27335
  function isAbsolutePathLike(value) {
26648
27336
  const trimmed = value.trim();
26649
- return path4.isAbsolute(trimmed) || isWindowsAbsolutePath(trimmed);
27337
+ return path5.isAbsolute(trimmed) || isWindowsAbsolutePath(trimmed);
26650
27338
  }
26651
27339
  function arePathsEqual(left, right) {
26652
27340
  return normalizePathForComparison(left) === normalizePathForComparison(right);
26653
27341
  }
26654
27342
  function arePathBasenamesEqual(left, right) {
26655
- const leftBase = path4.posix.basename(normalizePathForComparison(left));
26656
- const rightBase = path4.posix.basename(normalizePathForComparison(right));
27343
+ const leftBase = path5.posix.basename(normalizePathForComparison(left));
27344
+ const rightBase = path5.posix.basename(normalizePathForComparison(right));
26657
27345
  if (shouldComparePathCaseInsensitive(left) || shouldComparePathCaseInsensitive(right)) {
26658
27346
  return leftBase.toLowerCase() === rightBase.toLowerCase();
26659
27347
  }
@@ -26665,11 +27353,11 @@ function normalizePathForComparison(value) {
26665
27353
  return "";
26666
27354
  }
26667
27355
  if (isWindowsAbsolutePath(trimmed)) {
26668
- const normalized2 = path4.win32.normalize(trimmed).replace(/\\/g, "/");
27356
+ const normalized2 = path5.win32.normalize(trimmed).replace(/\\/g, "/");
26669
27357
  return trimTrailingPathSeparators(normalized2).toLowerCase();
26670
27358
  }
26671
- const pathForComparison = path4.isAbsolute(trimmed) ? trimmed : trimmed.replace(/\\/g, "/");
26672
- const normalized = path4.posix.normalize(pathForComparison);
27359
+ const pathForComparison = path5.isAbsolute(trimmed) ? trimmed : trimmed.replace(/\\/g, "/");
27360
+ const normalized = path5.posix.normalize(pathForComparison);
26673
27361
  return trimTrailingPathSeparators(normalized);
26674
27362
  }
26675
27363
  function isWindowsAbsolutePath(value) {
@@ -26692,7 +27380,7 @@ function isRootPath(value) {
26692
27380
 
26693
27381
  // src/AgentFileChangeReport.ts
26694
27382
  import fs7 from "node:fs";
26695
- import path5 from "node:path";
27383
+ import path6 from "node:path";
26696
27384
  var AGENT_FILE_CHANGE_REPORT_VERSION = 1;
26697
27385
  var AGENT_FILE_CHANGE_REPORT_TIMEOUT_MS = 3e4;
26698
27386
  var AGENT_FILE_CHANGE_REPORT_MAX_PATHS = 1024;
@@ -26702,7 +27390,7 @@ var AGENT_FILE_CHANGE_REPORT_MAX_UNCERTAINTY_LENGTH = 2e3;
26702
27390
  var AGENT_FILE_CHANGE_REPORT_OUTPUT_SCHEMA = {
26703
27391
  type: "object",
26704
27392
  additionalProperties: false,
26705
- required: ["paths", "complete"],
27393
+ required: ["paths", "complete", "uncertainty"],
26706
27394
  properties: {
26707
27395
  paths: {
26708
27396
  type: "array",
@@ -26710,8 +27398,12 @@ var AGENT_FILE_CHANGE_REPORT_OUTPUT_SCHEMA = {
26710
27398
  },
26711
27399
  complete: { type: "boolean" },
26712
27400
  uncertainty: {
26713
- type: "string",
26714
- maxLength: AGENT_FILE_CHANGE_REPORT_MAX_UNCERTAINTY_LENGTH
27401
+ anyOf: [{
27402
+ type: "string",
27403
+ maxLength: AGENT_FILE_CHANGE_REPORT_MAX_UNCERTAINTY_LENGTH
27404
+ }, {
27405
+ type: "null"
27406
+ }]
26715
27407
  }
26716
27408
  }
26717
27409
  };
@@ -26771,7 +27463,10 @@ function parseModelFileChangeReport(turn) {
26771
27463
  case "interrupted":
26772
27464
  throw new AgentFileChangeReportError("cancelled", "The audit turn was interrupted");
26773
27465
  case "failed":
26774
- throw new AgentFileChangeReportError("providerError", "The audit turn failed");
27466
+ throw new AgentFileChangeReportError(
27467
+ "providerError",
27468
+ `The audit turn failed${turn.error?.message ? `: ${turn.error.message}` : ""}`
27469
+ );
26775
27470
  case "inProgress":
26776
27471
  throw new AgentFileChangeReportError("notReported", "The audit turn did not complete");
26777
27472
  case "completed":
@@ -26801,10 +27496,10 @@ function parseModelFileChangeReport(turn) {
26801
27496
  const paths = report["paths"];
26802
27497
  const complete = report["complete"];
26803
27498
  const uncertainty = report["uncertainty"];
26804
- if (!Array.isArray(paths) || !paths.every((item) => typeof item === "string") || typeof complete !== "boolean" || uncertainty !== void 0 && typeof uncertainty !== "string") {
27499
+ if (!Array.isArray(paths) || !paths.every((item) => typeof item === "string") || typeof complete !== "boolean" || uncertainty !== void 0 && uncertainty !== null && typeof uncertainty !== "string") {
26805
27500
  throw new AgentFileChangeReportError("invalidOutput", "The audit turn returned invalid fields");
26806
27501
  }
26807
- const normalizedUncertainty = uncertainty?.trim();
27502
+ const normalizedUncertainty = typeof uncertainty === "string" ? uncertainty.trim() : void 0;
26808
27503
  if (normalizedUncertainty !== void 0 && normalizedUncertainty.length > AGENT_FILE_CHANGE_REPORT_MAX_UNCERTAINTY_LENGTH) {
26809
27504
  throw new AgentFileChangeReportError("invalidOutput", "The audit turn returned oversized uncertainty");
26810
27505
  }
@@ -26883,13 +27578,13 @@ function normalizeWorkspaceRoot(value) {
26883
27578
  }
26884
27579
  if (isWindowsAbsolutePath2(trimmed)) {
26885
27580
  return canonicalizeWorkspaceRoot({
26886
- value: path5.win32.normalize(trimmed.replace(/\//g, "\\")),
27581
+ value: path6.win32.normalize(trimmed.replace(/\//g, "\\")),
26887
27582
  flavor: "windows"
26888
27583
  });
26889
27584
  }
26890
- if (path5.posix.isAbsolute(trimmed)) {
27585
+ if (path6.posix.isAbsolute(trimmed)) {
26891
27586
  return canonicalizeWorkspaceRoot({
26892
- value: path5.posix.normalize(trimmed.replace(/\\/g, "/")),
27587
+ value: path6.posix.normalize(trimmed.replace(/\\/g, "/")),
26893
27588
  flavor: "posix"
26894
27589
  });
26895
27590
  }
@@ -26902,17 +27597,17 @@ function normalizeReportedPath(value, cwd, roots) {
26902
27597
  }
26903
27598
  let candidate;
26904
27599
  if (isWindowsAbsolutePath2(trimmed)) {
26905
- candidate = { value: path5.win32.normalize(trimmed.replace(/\//g, "\\")), flavor: "windows" };
26906
- } else if (path5.posix.isAbsolute(trimmed)) {
26907
- candidate = { value: path5.posix.normalize(trimmed.replace(/\\/g, "/")), flavor: "posix" };
27600
+ candidate = { value: path6.win32.normalize(trimmed.replace(/\//g, "\\")), flavor: "windows" };
27601
+ } else if (path6.posix.isAbsolute(trimmed)) {
27602
+ candidate = { value: path6.posix.normalize(trimmed.replace(/\\/g, "/")), flavor: "posix" };
26908
27603
  } else if (cwd.flavor === "windows") {
26909
27604
  candidate = {
26910
- value: path5.win32.resolve(cwd.value, trimmed.replace(/\//g, "\\")),
27605
+ value: path6.win32.resolve(cwd.value, trimmed.replace(/\//g, "\\")),
26911
27606
  flavor: "windows"
26912
27607
  };
26913
27608
  } else {
26914
27609
  candidate = {
26915
- value: path5.posix.resolve(cwd.value, trimmed.replace(/\\/g, "/")),
27610
+ value: path6.posix.resolve(cwd.value, trimmed.replace(/\\/g, "/")),
26916
27611
  flavor: "posix"
26917
27612
  };
26918
27613
  }
@@ -26929,23 +27624,23 @@ function canonicalizeReportedPath(candidate) {
26929
27624
  if (!isNativePathFlavor(candidate.flavor)) {
26930
27625
  return candidate;
26931
27626
  }
26932
- const parent = canonicalizeFromExistingAncestor(path5.dirname(candidate.value));
26933
- return { ...candidate, value: path5.resolve(parent, path5.basename(candidate.value)) };
27627
+ const parent = canonicalizeFromExistingAncestor(path6.dirname(candidate.value));
27628
+ return { ...candidate, value: path6.resolve(parent, path6.basename(candidate.value)) };
26934
27629
  }
26935
27630
  function canonicalizeFromExistingAncestor(value) {
26936
- const original = path5.resolve(value);
27631
+ const original = path6.resolve(value);
26937
27632
  let current = original;
26938
27633
  const missingSegments = [];
26939
27634
  while (true) {
26940
27635
  try {
26941
27636
  const canonical = fs7.realpathSync.native(current);
26942
- return path5.resolve(canonical, ...missingSegments.reverse());
27637
+ return path6.resolve(canonical, ...missingSegments.reverse());
26943
27638
  } catch (error51) {
26944
27639
  if (!isMissingPathError(error51)) return original;
26945
27640
  }
26946
- const parent = path5.dirname(current);
27641
+ const parent = path6.dirname(current);
26947
27642
  if (parent === current) return original;
26948
- missingSegments.push(path5.basename(current));
27643
+ missingSegments.push(path6.basename(current));
26949
27644
  current = parent;
26950
27645
  }
26951
27646
  }
@@ -26960,7 +27655,7 @@ function pathIsStrictlyInside(root, candidate) {
26960
27655
  if (root.flavor !== candidate.flavor) {
26961
27656
  return false;
26962
27657
  }
26963
- const pathImplementation = root.flavor === "windows" ? path5.win32 : path5.posix;
27658
+ const pathImplementation = root.flavor === "windows" ? path6.win32 : path6.posix;
26964
27659
  const relative = pathImplementation.relative(root.value, candidate.value);
26965
27660
  return relative.length > 0 && !pathImplementation.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${pathImplementation.sep}`);
26966
27661
  }
@@ -26979,6 +27674,120 @@ function hasOnlyKeys(value, allowed) {
26979
27674
  return Object.keys(value).every((key) => allowedKeys.has(key));
26980
27675
  }
26981
27676
 
27677
+ // src/subagents/CodexSubagentSubscriptions.ts
27678
+ var CodexSubagentSubscriptions = class {
27679
+ constructor(client) {
27680
+ this.client = client;
27681
+ }
27682
+ client;
27683
+ sessions = /* @__PURE__ */ new Map();
27684
+ subscribe(subscription) {
27685
+ const existing = this.sessions.get(subscription.rootSessionId);
27686
+ if (existing) {
27687
+ existing.current = subscription;
27688
+ return;
27689
+ }
27690
+ const session = { current: subscription, children: /* @__PURE__ */ new Set() };
27691
+ this.sessions.set(subscription.rootSessionId, session);
27692
+ this.client.onServerNotification(subscription.rootSessionId, (event) => {
27693
+ this.discover(session, event);
27694
+ session.current.dispatch(event);
27695
+ });
27696
+ this.registerInteractiveHandlers(session, subscription.rootSessionId);
27697
+ }
27698
+ clear(rootSessionId) {
27699
+ for (const childSessionId of this.sessions.get(rootSessionId)?.children ?? []) {
27700
+ this.client.clearThreadHandlers(childSessionId);
27701
+ }
27702
+ this.sessions.delete(rootSessionId);
27703
+ }
27704
+ discover(session, event) {
27705
+ if (event.method !== "item/started" && event.method !== "item/completed") {
27706
+ return;
27707
+ }
27708
+ const item = event.params.item;
27709
+ const childSessionIds = item.type === "collabAgentToolCall" && item.tool === "spawnAgent" ? item.receiverThreadIds : item.type === "subAgentActivity" && item.kind !== "interrupted" && !isRootAgentPath(item.agentPath) ? [item.agentThreadId] : [];
27710
+ for (const childSessionId of childSessionIds) {
27711
+ if (childSessionId.trim() === "") continue;
27712
+ if (childSessionId === session.current.rootSessionId || childSessionId === event.params.threadId || session.children.has(childSessionId)) {
27713
+ continue;
27714
+ }
27715
+ session.children.add(childSessionId);
27716
+ this.client.onServerNotification(childSessionId, (childEvent) => {
27717
+ const eventThreadId = childEvent.params.threadId;
27718
+ if (eventThreadId !== childSessionId) return;
27719
+ this.discover(session, childEvent);
27720
+ if (session.current.supportsSubagents) session.current.dispatch(childEvent);
27721
+ else session.current.enqueueInteraction(this.rootAttributed(childEvent, session.current.rootSessionId));
27722
+ });
27723
+ this.registerInteractiveHandlers(session, childSessionId);
27724
+ }
27725
+ }
27726
+ registerInteractiveHandlers(session, targetSessionId) {
27727
+ this.client.onApprovalRequest(targetSessionId, {
27728
+ handleCommandExecution: async (params) => {
27729
+ const current = session.current;
27730
+ await current.waitForRootNotifications();
27731
+ const sessionId = await this.interactionSessionId(current, targetSessionId);
27732
+ if (sessionId === null) return { decision: "cancel" };
27733
+ return await current.approvalHandler.handleCommandExecution(
27734
+ { ...params, threadId: sessionId }
27735
+ );
27736
+ },
27737
+ handleFileChange: async (params) => {
27738
+ const current = session.current;
27739
+ await current.waitForRootNotifications();
27740
+ const sessionId = await this.interactionSessionId(current, targetSessionId);
27741
+ if (sessionId === null) return { decision: "cancel" };
27742
+ return await current.approvalHandler.handleFileChange(
27743
+ { ...params, threadId: sessionId }
27744
+ );
27745
+ },
27746
+ handlePermissionsRequest: async (params) => {
27747
+ const current = session.current;
27748
+ await current.waitForRootNotifications();
27749
+ const sessionId = await this.interactionSessionId(current, targetSessionId);
27750
+ if (sessionId === null) return { permissions: {}, scope: "turn", strictAutoReview: false };
27751
+ return await current.approvalHandler.handlePermissionsRequest(
27752
+ { ...params, threadId: sessionId }
27753
+ );
27754
+ }
27755
+ });
27756
+ this.client.onElicitationRequest(targetSessionId, {
27757
+ handleElicitation: async (params) => {
27758
+ const current = session.current;
27759
+ await current.waitForRootNotifications();
27760
+ const sessionId = await this.interactionSessionId(current, targetSessionId);
27761
+ if (sessionId === null) return { action: "cancel", content: null, _meta: null };
27762
+ return await current.elicitationHandler.handleElicitation(
27763
+ { ...params, threadId: sessionId }
27764
+ );
27765
+ },
27766
+ handleUserInput: async (params) => {
27767
+ const current = session.current;
27768
+ await current.waitForRootNotifications();
27769
+ const sessionId = await this.interactionSessionId(current, targetSessionId);
27770
+ if (sessionId === null) return { answers: {} };
27771
+ return await current.elicitationHandler.handleUserInput(
27772
+ { ...params, threadId: sessionId }
27773
+ );
27774
+ }
27775
+ });
27776
+ }
27777
+ async interactionSessionId(subscription, targetSessionId) {
27778
+ if (targetSessionId === subscription.rootSessionId) return targetSessionId;
27779
+ if (!subscription.supportsSubagents) return subscription.rootSessionId;
27780
+ return await subscription.waitForChildSession(targetSessionId);
27781
+ }
27782
+ rootAttributed(event, rootSessionId) {
27783
+ if (typeof event.params.threadId !== "string") return event;
27784
+ return {
27785
+ ...event,
27786
+ params: { ...event.params, threadId: rootSessionId }
27787
+ };
27788
+ }
27789
+ };
27790
+
26982
27791
  // src/CodexAcpClient.ts
26983
27792
  var CUSTOM_GATEWAY_PROVIDER_ID = "custom-gateway";
26984
27793
  var OPENAI_PROVIDER_ID = "openai";
@@ -26994,6 +27803,7 @@ var CodexAcpClient = class {
26994
27803
  pendingLoginCompleted = null;
26995
27804
  pendingAccountUpdated = null;
26996
27805
  sessionNotificationQueues = /* @__PURE__ */ new Map();
27806
+ subagents;
26997
27807
  skillExtraRoots = [];
26998
27808
  configPath = null;
26999
27809
  constructor(codexClient, codexConfig, modelProvider) {
@@ -27001,6 +27811,7 @@ var CodexAcpClient = class {
27001
27811
  this.config = codexConfig ?? {};
27002
27812
  this.modelProvider = modelProvider ?? null;
27003
27813
  this.gatewayConfig = null;
27814
+ this.subagents = new CodexSubagentSubscriptions(codexClient);
27004
27815
  }
27005
27816
  defaultClientInfo = {
27006
27817
  name: `${package_default.name}`,
@@ -27076,16 +27887,31 @@ var CodexAcpClient = class {
27076
27887
  if (loginResponse.type !== "chatgptDeviceCode") {
27077
27888
  return false;
27078
27889
  }
27079
- const elicitationResponse = await urlElicitationRequester.elicitUrl({
27890
+ const elicitationResponsePromise = Promise.resolve(urlElicitationRequester.elicitUrl({
27080
27891
  url: loginResponse.verificationUrl,
27081
27892
  message: `Sign in to ChatGPT and enter this code: ${loginResponse.userCode}`,
27082
27893
  elicitationId: loginResponse.loginId
27083
- });
27084
- if (!CreateElicitationResponse.isAccept(elicitationResponse)) {
27894
+ }));
27895
+ const first = await Promise.race([
27896
+ loginCompletedPromise.then((result2) => ({
27897
+ type: "loginCompleted",
27898
+ result: result2
27899
+ })),
27900
+ elicitationResponsePromise.then((response) => ({
27901
+ type: "elicitationResponse",
27902
+ response
27903
+ }))
27904
+ ]);
27905
+ if (first.type === "loginCompleted") {
27906
+ await urlElicitationRequester.completeElicitation();
27907
+ return first.result.success;
27908
+ }
27909
+ if (!CreateElicitationResponse.isAccept(first.response)) {
27085
27910
  await this.codexClient.accountLoginCancel({ loginId: loginResponse.loginId });
27086
27911
  return false;
27087
27912
  }
27088
27913
  const result = await loginCompletedPromise;
27914
+ await urlElicitationRequester.completeElicitation();
27089
27915
  return result.success;
27090
27916
  }
27091
27917
  authenticateWithGateway(authRequest) {
@@ -27331,6 +28157,12 @@ var CodexAcpClient = class {
27331
28157
  additionalDirectories
27332
28158
  };
27333
28159
  }
28160
+ async readSessionThread(sessionId) {
28161
+ return (await this.codexClient.threadRead({
28162
+ threadId: sessionId,
28163
+ includeTurns: true
28164
+ })).thread;
28165
+ }
27334
28166
  async newSession(request) {
27335
28167
  const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta);
27336
28168
  await this.refreshSkills(request.cwd, additionalDirectories);
@@ -27359,6 +28191,7 @@ var CodexAcpClient = class {
27359
28191
  await this.codexClient.threadUnsubscribe({ threadId: sessionId });
27360
28192
  } finally {
27361
28193
  this.codexClient.clearThreadHandlers(sessionId);
28194
+ this.subagents.clear(sessionId);
27362
28195
  }
27363
28196
  }
27364
28197
  async deleteSession(sessionId) {
@@ -27484,7 +28317,7 @@ var CodexAcpClient = class {
27484
28317
  if (!cwd) {
27485
28318
  return;
27486
28319
  }
27487
- const skillExtraRoots = additionalRoots.map((root) => path6.join(root, ".agents", "skills"));
28320
+ const skillExtraRoots = additionalRoots.map((root) => path7.join(root, ".agents", "skills"));
27488
28321
  if (!arraysEqual(this.skillExtraRoots, skillExtraRoots)) {
27489
28322
  await this.codexClient.skillsExtraRootsSet({ extraRoots: skillExtraRoots });
27490
28323
  this.skillExtraRoots = skillExtraRoots;
@@ -27535,33 +28368,21 @@ var CodexAcpClient = class {
27535
28368
  }
27536
28369
  return ModelId.create(defaultModel.id, reasoningEffort ?? defaultModel.defaultReasoningEffort);
27537
28370
  }
27538
- async subscribeToSessionEvents(sessionId, eventHandler, approvalHandler, elicitationHandler) {
27539
- this.codexClient.onServerNotification(sessionId, (event) => {
28371
+ async subscribeToSessionEvents(sessionId, eventHandler, approvalHandler, elicitationHandler, supportsSubagents, observeInteraction, waitForChildSession) {
28372
+ const dispatch = (event) => {
27540
28373
  this.enqueueSessionNotification(sessionId, () => eventHandler(event));
27541
- });
27542
- this.codexClient.onApprovalRequest(sessionId, {
27543
- handleCommandExecution: async (params) => {
27544
- await this.waitForSessionNotifications(sessionId);
27545
- return await approvalHandler.handleCommandExecution(params);
27546
- },
27547
- handleFileChange: async (params) => {
27548
- await this.waitForSessionNotifications(sessionId);
27549
- return await approvalHandler.handleFileChange(params);
27550
- },
27551
- handlePermissionsRequest: async (params) => {
27552
- await this.waitForSessionNotifications(sessionId);
27553
- return await approvalHandler.handlePermissionsRequest(params);
27554
- }
27555
- });
27556
- this.codexClient.onElicitationRequest(sessionId, {
27557
- handleElicitation: async (params) => {
27558
- await this.waitForSessionNotifications(sessionId);
27559
- return await elicitationHandler.handleElicitation(params);
28374
+ };
28375
+ this.subagents.subscribe({
28376
+ rootSessionId: sessionId,
28377
+ supportsSubagents,
28378
+ dispatch,
28379
+ enqueueInteraction: (event) => {
28380
+ this.enqueueSessionNotification(sessionId, () => observeInteraction(event));
27560
28381
  },
27561
- handleUserInput: async (params) => {
27562
- await this.waitForSessionNotifications(sessionId);
27563
- return await elicitationHandler.handleUserInput(params);
27564
- }
28382
+ approvalHandler,
28383
+ elicitationHandler,
28384
+ waitForRootNotifications: () => this.waitForSessionNotifications(sessionId),
28385
+ waitForChildSession
27565
28386
  });
27566
28387
  }
27567
28388
  async waitForSessionNotifications(sessionId) {
@@ -27599,6 +28420,7 @@ var CodexAcpClient = class {
27599
28420
  threadId: request.sessionId,
27600
28421
  input,
27601
28422
  approvalPolicy: agentMode.approvalPolicy,
28423
+ approvalsReviewer: agentMode.approvalsReviewer,
27602
28424
  sandboxPolicy: addAdditionalDirectoriesToSandboxPolicy(agentMode.sandboxPolicy, additionalDirectories),
27603
28425
  summary: disableSummary ? "none" : "auto",
27604
28426
  effort,
@@ -27653,22 +28475,9 @@ var CodexAcpClient = class {
27653
28475
  });
27654
28476
  const outcome = await budget.wait(turnPromise);
27655
28477
  auditTurnCompleted = true;
27656
- const thread = await budget.wait(this.codexClient.threadRead({
27657
- threadId: forkThreadId,
27658
- includeTurns: true
27659
- }));
27660
- const completedTurn = thread.thread.turns.find(
27661
- (turn) => turn.id === outcome.turn.id
27662
- );
27663
- if (completedTurn === void 0) {
27664
- throw new AgentFileChangeReportError(
27665
- "notReported",
27666
- "The completed audit turn was not present in thread history"
27667
- );
27668
- }
27669
28478
  return createReportedAgentFileChangeReport(
27670
28479
  params.requestId,
27671
- completedTurn,
28480
+ outcome.turn,
27672
28481
  params.workspace
27673
28482
  );
27674
28483
  } catch (error51) {
@@ -27685,7 +28494,7 @@ var CodexAcpClient = class {
27685
28494
  return createUnavailableAgentFileChangeReport(params.requestId, error51.reason);
27686
28495
  }
27687
28496
  if (error51 instanceof AgentFileChangeReportError) {
27688
- logger.log("Agent file-change report unavailable", { reason: error51.reason });
28497
+ logger.error("Agent file-change report unavailable", error51);
27689
28498
  return createUnavailableAgentFileChangeReport(params.requestId, error51.reason);
27690
28499
  }
27691
28500
  logger.error("Agent file-change report failed", error51);
@@ -27974,8 +28783,8 @@ function formatUriAsLink(name, uri) {
27974
28783
  return `[@${name}](${uri})`;
27975
28784
  }
27976
28785
  if (uri.startsWith("file://")) {
27977
- const path8 = uri.replace("file://", "");
27978
- const fileName = path8.split("/").pop() ?? path8;
28786
+ const path9 = uri.replace("file://", "");
28787
+ const fileName = path9.split("/").pop() ?? path9;
27979
28788
  return `[@${fileName}](${uri})`;
27980
28789
  }
27981
28790
  return uri;
@@ -28005,7 +28814,7 @@ function readAdditionalDirectories(cwd, additionalDirectories, meta3) {
28005
28814
  if (directory.length === 0) {
28006
28815
  throw RequestError.invalidParams(void 0, "additionalDirectories entries must not be empty");
28007
28816
  }
28008
- if (!path6.isAbsolute(directory)) {
28817
+ if (!path7.isAbsolute(directory)) {
28009
28818
  throw RequestError.invalidParams(void 0, "additionalDirectories entries must be absolute paths");
28010
28819
  }
28011
28820
  if (!seen.has(directory)) {
@@ -28160,11 +28969,11 @@ var CodexAppServerClient = class {
28160
28969
  });
28161
28970
  this.connection.onRequest(PermissionsApprovalRequest, async (params) => {
28162
28971
  if (this.isStaleTurn(params.threadId, params.turnId)) {
28163
- return { permissions: {}, scope: "turn", strictAutoReview: true };
28972
+ return { permissions: {}, scope: "turn", strictAutoReview: false };
28164
28973
  }
28165
28974
  const handler = this.approvalHandlers.get(params.threadId);
28166
28975
  if (!handler) {
28167
- return { permissions: {}, scope: "turn", strictAutoReview: true };
28976
+ return { permissions: {}, scope: "turn", strictAutoReview: false };
28168
28977
  }
28169
28978
  return await handler.handlePermissionsRequest(params);
28170
28979
  });
@@ -28927,11 +29736,14 @@ var CodexCommands = class {
28927
29736
  this.runWithProcessCheck = runWithProcessCheck;
28928
29737
  this.onLogout = onLogout;
28929
29738
  }
28930
- async publish(sessionState) {
29739
+ async publish(sessionState, shouldPublish = () => true) {
28931
29740
  try {
29741
+ if (!shouldPublish()) {
29742
+ return;
29743
+ }
28932
29744
  const skillsResponse = await this.runWithProcessCheck(() => this.codexAcpClient.listSkills(this.createSkillsListParams(sessionState)));
28933
29745
  const availableCommands = this.buildAvailableCommands(skillsResponse?.data ?? []);
28934
- if (availableCommands.length === 0) {
29746
+ if (availableCommands.length === 0 || !shouldPublish()) {
28935
29747
  return;
28936
29748
  }
28937
29749
  const session = new ACPSessionConnection(this.connection, sessionState.sessionId);
@@ -28940,7 +29752,9 @@ var CodexCommands = class {
28940
29752
  availableCommands
28941
29753
  });
28942
29754
  } catch (err) {
28943
- logger.error(`Failed to publish available commands for session ${sessionState.sessionId}`, err);
29755
+ if (shouldPublish()) {
29756
+ logger.error(`Failed to publish available commands for session ${sessionState.sessionId}`, err);
29757
+ }
28944
29758
  }
28945
29759
  }
28946
29760
  createSkillsListParams(sessionState) {
@@ -29399,7 +30213,7 @@ var SteeringQueue = class {
29399
30213
 
29400
30214
  // src/ResponseItemHistoryFallback.ts
29401
30215
  import { readFile as readFile2 } from "node:fs/promises";
29402
- import path7 from "node:path";
30216
+ import path8 from "node:path";
29403
30217
  function historyFallbackUpdateKey(update) {
29404
30218
  switch (update.sessionUpdate) {
29405
30219
  case "user_message_chunk":
@@ -29639,8 +30453,8 @@ function imageBlocks(images) {
29639
30453
  return [{ type: "text", text: `[@image](${image})` }];
29640
30454
  }
29641
30455
  const record2 = asRecord2(image);
29642
- const path8 = record2 ? stringValue(record2["path"]) ?? stringValue(record2["url"]) : null;
29643
- return path8 ? [{ type: "text", text: `[@image](${path8})` }] : [];
30456
+ const path9 = record2 ? stringValue(record2["path"]) ?? stringValue(record2["url"]) : null;
30457
+ return path9 ? [{ type: "text", text: `[@image](${path9})` }] : [];
29644
30458
  });
29645
30459
  }
29646
30460
  function contentBlocksFromResponseContent(content) {
@@ -30272,10 +31086,10 @@ function baseCommandName(commandName) {
30272
31086
  return commandName.split(/[\\/]/).pop() ?? commandName;
30273
31087
  }
30274
31088
  function absolutizePath(cwd, targetPath) {
30275
- if (path7.isAbsolute(targetPath) || cwd.length === 0) {
31089
+ if (path8.isAbsolute(targetPath) || cwd.length === 0) {
30276
31090
  return targetPath;
30277
31091
  }
30278
- return path7.join(cwd, targetPath);
31092
+ return path8.join(cwd, targetPath);
30279
31093
  }
30280
31094
  function withTerminalContent(event, terminalId, cwd) {
30281
31095
  const { rawInput, ...eventWithoutRawInput } = event;
@@ -30433,16 +31247,7 @@ function clientSupportsPlanUpdates(clientCapabilities) {
30433
31247
  // src/CodexAcpServer.ts
30434
31248
  import { randomUUID as randomUUID2 } from "node:crypto";
30435
31249
  import { once } from "node:events";
30436
- var IMPLEMENT_PLAN_OPTION_ID = "implement_plan";
30437
- var REVISE_PLAN_OPTION_ID = "revise_plan";
30438
31250
  var CODEX_PROCESS_EXITED_ERROR_CODE = 1001;
30439
- function clientSupportsAirCapability(capabilities, capability) {
30440
- const jetbrains = capabilities?._meta?.[JETBRAINS_META_KEY];
30441
- const air = jetbrains?.[AIR_META_KEY];
30442
- const version2 = air?.[AIR_EXTENSION_VERSION_KEY];
30443
- const supported = air?.[AIR_EXTENSION_CAPABILITIES_KEY];
30444
- return typeof version2 === "number" && Number.isInteger(version2) && version2 >= AIR_EXTENSION_VERSION && Array.isArray(supported) && supported.includes(capability);
30445
- }
30446
31251
  function clientSupportsTypedSessionFailures(capabilities) {
30447
31252
  return clientSupportsAirCapability(capabilities, AIR_SESSION_FAILURE_KEY);
30448
31253
  }
@@ -30475,6 +31280,7 @@ var CodexAcpServer = class _CodexAcpServer {
30475
31280
  sessionGenerations;
30476
31281
  sessionOpenGenerations;
30477
31282
  goalControlGenerations;
31283
+ permissionLifecycleContexts;
30478
31284
  codexProcessState;
30479
31285
  initializeRequest = null;
30480
31286
  providerUpdate = null;
@@ -30488,6 +31294,7 @@ var CodexAcpServer = class _CodexAcpServer {
30488
31294
  this.sessionGenerations = /* @__PURE__ */ new Map();
30489
31295
  this.sessionOpenGenerations = /* @__PURE__ */ new Map();
30490
31296
  this.goalControlGenerations = /* @__PURE__ */ new Map();
31297
+ this.permissionLifecycleContexts = /* @__PURE__ */ new WeakMap();
30491
31298
  this.connection = connection;
30492
31299
  this.codexAcpClient = codexAcpClient;
30493
31300
  this.defaultAuthRequest = defaultAuthRequest ?? null;
@@ -30518,6 +31325,14 @@ var CodexAcpServer = class _CodexAcpServer {
30518
31325
  this.terminalOutputMode = resolveTerminalOutputMode(_params.clientCapabilities);
30519
31326
  this.booleanConfigOptionsSupported = clientSupportsBooleanConfigOptions(_params.clientCapabilities);
30520
31327
  await this.runWithProcessCheck(() => this.codexAcpClient.initialize(_params));
31328
+ const sessionCapabilities = {
31329
+ resume: {},
31330
+ list: {},
31331
+ close: {},
31332
+ delete: {},
31333
+ additionalDirectories: {},
31334
+ subagents: {}
31335
+ };
30521
31336
  return {
30522
31337
  protocolVersion: PROTOCOL_VERSION,
30523
31338
  agentInfo: {
@@ -30535,13 +31350,7 @@ var CodexAcpServer = class _CodexAcpServer {
30535
31350
  embeddedContext: true,
30536
31351
  image: true
30537
31352
  },
30538
- sessionCapabilities: {
30539
- resume: {},
30540
- list: {},
30541
- close: {},
30542
- delete: {},
30543
- additionalDirectories: {}
30544
- },
31353
+ sessionCapabilities,
30545
31354
  mcpCapabilities: {
30546
31355
  acp: false,
30547
31356
  http: true,
@@ -30563,7 +31372,8 @@ var CodexAcpServer = class _CodexAcpServer {
30563
31372
  [AIR_EXTENSION_VERSION_KEY]: AIR_EXTENSION_VERSION,
30564
31373
  [AIR_EXTENSION_CAPABILITIES_KEY]: [
30565
31374
  AIR_SESSION_FAILURE_KEY,
30566
- AIR_AGENT_FILE_CHANGE_REPORT_KEY
31375
+ AIR_AGENT_FILE_CHANGE_REPORT_KEY,
31376
+ AIR_NATIVE_SUBAGENT_SESSIONS_KEY
30567
31377
  ]
30568
31378
  }
30569
31379
  }
@@ -30615,7 +31425,7 @@ var CodexAcpServer = class _CodexAcpServer {
30615
31425
  }
30616
31426
  } else if (methodRequest.params.action === "pause") {
30617
31427
  const goal = await this.runWithProcessCheck(() => this.codexAcpClient.setGoalStatus(sessionState.sessionId, "paused"));
30618
- if (this.goalPublishIsCurrent(sessionState, sessionGeneration)) {
31428
+ if (this.sessionPublishIsCurrent(sessionState, sessionGeneration)) {
30619
31429
  await this.publishGoalSnapshot(sessionState, toThreadGoalSnapshot(goal), false);
30620
31430
  }
30621
31431
  } else if (methodRequest.params.action === "resume") {
@@ -30627,7 +31437,7 @@ var CodexAcpServer = class _CodexAcpServer {
30627
31437
  updatedGoal = goal;
30628
31438
  }
30629
31439
  ));
30630
- if (updatedGoal !== null && this.goalPublishIsCurrent(sessionState, sessionGeneration)) {
31440
+ if (updatedGoal !== null && this.sessionPublishIsCurrent(sessionState, sessionGeneration)) {
30631
31441
  await this.publishGoalSnapshot(sessionState, toThreadGoalSnapshot(updatedGoal), false);
30632
31442
  }
30633
31443
  if (turnCompleted === null && updatedGoal !== null) {
@@ -30640,7 +31450,7 @@ var CodexAcpServer = class _CodexAcpServer {
30640
31450
  }
30641
31451
  } else if (methodRequest.params.action === "clear") {
30642
31452
  await this.runWithProcessCheck(() => this.codexAcpClient.clearGoal(sessionState.sessionId));
30643
- if (this.goalPublishIsCurrent(sessionState, sessionGeneration)) {
31453
+ if (this.sessionPublishIsCurrent(sessionState, sessionGeneration)) {
30644
31454
  await this.publishGoalSnapshot(sessionState, null, false);
30645
31455
  }
30646
31456
  }
@@ -30816,7 +31626,12 @@ Check ${configPath} and project .codex directories, especially their config.toml
30816
31626
  terminalOutputMode: this.terminalOutputMode,
30817
31627
  goalRevision: 0,
30818
31628
  sessionTitle: null,
30819
- sessionTitleSource: "sessionId" in request ? "unknown" : "unset"
31629
+ sessionTitleSource: "sessionId" in request ? "unknown" : "unset",
31630
+ subagents: new CodexSubagentEventRouter(
31631
+ sessionId,
31632
+ clientSupportsSubagents(this.clientCapabilities),
31633
+ new ACPSessionConnection(this.connection, sessionId)
31634
+ )
30820
31635
  };
30821
31636
  this.sessions.set(sessionId, sessionState);
30822
31637
  resumeSubscribed = false;
@@ -30827,7 +31642,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
30827
31642
  });
30828
31643
  this.publishMcpStartupStatusAsync(sessionId);
30829
31644
  }
30830
- this.publishAvailableCommandsAsync(sessionState);
31645
+ this.publishAvailableCommandsAsync(sessionState, sessionGeneration);
30831
31646
  if ("sessionId" in request) {
30832
31647
  this.publishCurrentGoalAsync(sessionState, sessionGeneration);
30833
31648
  }
@@ -31010,12 +31825,24 @@ Check ${configPath} and project .codex directories, especially their config.toml
31010
31825
  if (requestId == null || !clientSupportsUrlElicitation(this.clientCapabilities)) {
31011
31826
  return void 0;
31012
31827
  }
31828
+ let elicitationId = null;
31013
31829
  return {
31014
- elicitUrl: (request) => this.connection.request(methods.client.elicitation.create, {
31015
- mode: "url",
31016
- requestId,
31017
- ...request
31018
- })
31830
+ elicitUrl: (request) => {
31831
+ elicitationId = request.elicitationId;
31832
+ return this.connection.request(methods.client.elicitation.create, {
31833
+ mode: "url",
31834
+ requestId,
31835
+ ...request
31836
+ });
31837
+ },
31838
+ completeElicitation: async () => {
31839
+ if (elicitationId === null) {
31840
+ return;
31841
+ }
31842
+ await this.connection.notify(methods.client.elicitation.complete, {
31843
+ elicitationId
31844
+ });
31845
+ }
31019
31846
  };
31020
31847
  }
31021
31848
  async logout(_params) {
@@ -31404,7 +32231,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
31404
32231
  sessionId: sessionState.sessionId,
31405
32232
  prompt: GOAL_CONTINUATION_PROMPT
31406
32233
  }, "Goal continuation", async () => {
31407
- if (!this.goalPublishIsCurrent(sessionState, sessionGeneration) || this.goalControlGenerations.get(sessionState.sessionId) !== goalControlGeneration) {
32234
+ if (!this.sessionPublishIsCurrent(sessionState, sessionGeneration) || this.goalControlGenerations.get(sessionState.sessionId) !== goalControlGeneration) {
31408
32235
  return false;
31409
32236
  }
31410
32237
  const currentGoal = await this.runWithProcessCheck(() => this.codexAcpClient.getGoal(sessionState.sessionId));
@@ -31515,8 +32342,14 @@ Check ${configPath} and project .codex directories, especially their config.toml
31515
32342
  isSessionConfigEnabled() {
31516
32343
  return !isJetBrains2026_1Client(this.clientInfo);
31517
32344
  }
31518
- publishAvailableCommandsAsync(sessionState) {
31519
- void this.availableCommands.publish(sessionState);
32345
+ publishAvailableCommandsAsync(sessionState, sessionGeneration) {
32346
+ void this.publishAvailableCommands(sessionState, sessionGeneration);
32347
+ }
32348
+ async publishAvailableCommands(sessionState, sessionGeneration) {
32349
+ await this.availableCommands.publish(
32350
+ sessionState,
32351
+ () => this.sessionPublishIsCurrent(sessionState, sessionGeneration)
32352
+ );
31520
32353
  }
31521
32354
  publishCurrentGoalAsync(sessionState, sessionGeneration) {
31522
32355
  void this.publishCurrentGoalBestEffort(sessionState, sessionGeneration, true);
@@ -31532,12 +32365,12 @@ Check ${configPath} and project .codex directories, especially their config.toml
31532
32365
  const requestRevision = ++sessionState.goalRevision;
31533
32366
  const goal = await this.runWithProcessCheck(() => this.codexAcpClient.getGoal(sessionState.sessionId));
31534
32367
  const snapshot = goal === null ? null : toThreadGoalSnapshot(goal);
31535
- if (!this.goalPublishIsCurrent(sessionState, sessionGeneration) || sessionState.goalRevision !== requestRevision) {
32368
+ if (!this.sessionPublishIsCurrent(sessionState, sessionGeneration) || sessionState.goalRevision !== requestRevision) {
31536
32369
  return;
31537
32370
  }
31538
32371
  await this.publishGoalSnapshot(sessionState, snapshot, force, false);
31539
32372
  }
31540
- goalPublishIsCurrent(sessionState, sessionGeneration) {
32373
+ sessionPublishIsCurrent(sessionState, sessionGeneration) {
31541
32374
  return this.sessions.get(sessionState.sessionId) === sessionState && this.getSessionGeneration(sessionState.sessionId) === sessionGeneration && !this.sessionIsClosing(sessionState.sessionId);
31542
32375
  }
31543
32376
  async publishGoalSnapshot(sessionState, snapshot, force, incrementRevision = true) {
@@ -31639,7 +32472,12 @@ Check ${configPath} and project .codex directories, especially their config.toml
31639
32472
  terminalOutputMode: this.terminalOutputMode,
31640
32473
  goalRevision: 0,
31641
32474
  sessionTitle: null,
31642
- sessionTitleSource: "unset"
32475
+ sessionTitleSource: "unset",
32476
+ subagents: new CodexSubagentEventRouter(
32477
+ sessionId,
32478
+ clientSupportsSubagents(this.clientCapabilities),
32479
+ new ACPSessionConnection(this.connection, sessionId)
32480
+ )
31643
32481
  };
31644
32482
  this.sessions.set(sessionId, sessionState);
31645
32483
  subscribed = false;
@@ -31650,7 +32488,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
31650
32488
  });
31651
32489
  this.publishMcpStartupStatusAsync(sessionId);
31652
32490
  }
31653
- await this.availableCommands.publish(sessionState);
32491
+ await this.publishAvailableCommands(sessionState, requestedSessionGeneration);
31654
32492
  await this.publishCurrentGoalBestEffort(sessionState, requestedSessionGeneration, true);
31655
32493
  const sessionModelState = this.createModelState(models, currentModelId);
31656
32494
  const sessionModeState = sessionState.agentMode.toSessionModeState();
@@ -31665,6 +32503,16 @@ Check ${configPath} and project .codex directories, especially their config.toml
31665
32503
  const session = new ACPSessionConnection(this.connection, sessionId);
31666
32504
  const sessionState = this.getSessionState(sessionId);
31667
32505
  await this.publishThreadHistoryTitle(session, sessionState, thread);
32506
+ if (clientSupportsSubagents(this.clientCapabilities)) {
32507
+ await this.streamNativeThreadHistory(
32508
+ sessionId,
32509
+ thread,
32510
+ sessionState,
32511
+ /* @__PURE__ */ new Set([sessionId]),
32512
+ /* @__PURE__ */ new Map([[sessionId, thread]])
32513
+ );
32514
+ return;
32515
+ }
31668
32516
  const responseItemFallbackUpdates = await createResponseItemHistoryFallbackUpdates(
31669
32517
  thread,
31670
32518
  sessionState.terminalOutputMode
@@ -31681,6 +32529,93 @@ Check ${configPath} and project .codex directories, especially their config.toml
31681
32529
  await session.update(update);
31682
32530
  }
31683
32531
  }
32532
+ async streamNativeThreadHistory(sessionId, thread, sessionState, ancestry, threadCache) {
32533
+ const session = new ACPSessionConnection(this.connection, sessionId);
32534
+ const announced = /* @__PURE__ */ new Map();
32535
+ for (const turn of thread.turns) {
32536
+ for (const item of turn.items) {
32537
+ if (item.type === "subAgentActivity") {
32538
+ const activityKind = item.kind;
32539
+ if (activityKind === "started") {
32540
+ const previous = announced.get(item.agentThreadId);
32541
+ if (previous && !previous.terminal) continue;
32542
+ const generation = (previous?.generation ?? 0) + 1;
32543
+ const childSessionId = generation === 1 ? item.agentThreadId : `${item.agentThreadId}:generation:${generation}`;
32544
+ const name = nameFromAgentPath(item.agentPath, `Agent ${item.agentThreadId.slice(-8)}`);
32545
+ await session.update({
32546
+ sessionUpdate: "subagent_spawned",
32547
+ subagentSessionId: childSessionId,
32548
+ name,
32549
+ task: `Delegated task for ${name}`,
32550
+ capabilities: {}
32551
+ });
32552
+ announced.set(item.agentThreadId, { generation, sessionId: childSessionId, terminal: false });
32553
+ if (!ancestry.has(item.agentThreadId)) {
32554
+ let child = threadCache.get(item.agentThreadId);
32555
+ if (child === void 0) {
32556
+ try {
32557
+ child = await this.codexAcpClient.readSessionThread(item.agentThreadId);
32558
+ threadCache.set(item.agentThreadId, child);
32559
+ } catch (error51) {
32560
+ threadCache.set(item.agentThreadId, null);
32561
+ logger.error(`Failed to read subagent history ${item.agentThreadId}`, error51);
32562
+ child = null;
32563
+ }
32564
+ }
32565
+ const childTurn = child?.turns[generation - 1];
32566
+ if (child && childTurn) {
32567
+ await this.streamNativeThreadHistory(
32568
+ childSessionId,
32569
+ { ...child, turns: [childTurn] },
32570
+ sessionState,
32571
+ /* @__PURE__ */ new Set([...ancestry, item.agentThreadId]),
32572
+ threadCache
32573
+ );
32574
+ }
32575
+ }
32576
+ } else if (activityKind === "completed" || activityKind === "interrupted") {
32577
+ const child = announced.get(item.agentThreadId);
32578
+ if (!child) {
32579
+ const name = nameFromAgentPath(item.agentPath, `Agent ${item.agentThreadId.slice(-8)}`);
32580
+ await session.update({
32581
+ sessionUpdate: "subagent_spawned",
32582
+ subagentSessionId: item.agentThreadId,
32583
+ name,
32584
+ task: `Delegated task for ${name}`,
32585
+ capabilities: {}
32586
+ });
32587
+ announced.set(item.agentThreadId, {
32588
+ generation: 1,
32589
+ sessionId: item.agentThreadId,
32590
+ terminal: false
32591
+ });
32592
+ continue;
32593
+ }
32594
+ if (child.terminal) continue;
32595
+ await session.update({
32596
+ sessionUpdate: "subagent_state_update",
32597
+ subagentSessionId: child.sessionId,
32598
+ state: activityKind === "completed" ? "completed" : "cancelled"
32599
+ });
32600
+ child.terminal = true;
32601
+ }
32602
+ continue;
32603
+ }
32604
+ if (item.type === "collabAgentToolCall") continue;
32605
+ for (const update of await this.createHistoryUpdates(item, sessionState)) {
32606
+ await session.update(update);
32607
+ }
32608
+ }
32609
+ }
32610
+ for (const child of announced.values()) {
32611
+ if (child.terminal) continue;
32612
+ await session.update({
32613
+ sessionUpdate: "subagent_state_update",
32614
+ subagentSessionId: child.sessionId,
32615
+ state: "disconnected"
32616
+ });
32617
+ }
32618
+ }
31684
32619
  async publishThreadHistoryTitle(session, sessionState, thread) {
31685
32620
  const explicitTitle = this.normalizeSessionTitle(thread.name);
31686
32621
  if (explicitTitle) {
@@ -31885,8 +32820,8 @@ Check ${configPath} and project .codex directories, especially their config.toml
31885
32820
  return `[@${name}](${uri})`;
31886
32821
  }
31887
32822
  if (uri.startsWith("file://")) {
31888
- const path8 = uri.replace("file://", "");
31889
- const fileName = path8.split("/").pop() ?? path8;
32823
+ const path9 = uri.replace("file://", "");
32824
+ const fileName = path9.split("/").pop() ?? path9;
31890
32825
  return `[@${fileName}](${uri})`;
31891
32826
  }
31892
32827
  return uri;
@@ -31898,6 +32833,13 @@ Check ${configPath} and project .codex directories, especially their config.toml
31898
32833
  }
31899
32834
  return sessionState;
31900
32835
  }
32836
+ permissionLifecycleContext(sessionState) {
32837
+ const existing = this.permissionLifecycleContexts.get(sessionState);
32838
+ if (existing) return existing;
32839
+ const context = new PermissionLifecycleContext(sessionState);
32840
+ this.permissionLifecycleContexts.set(sessionState, context);
32841
+ return context;
32842
+ }
31901
32843
  resolveSessionMcpServers(mcpServers, recoverFromStartup) {
31902
32844
  const requestedServerNames = getRequestedMcpServerNames(mcpServers);
31903
32845
  if (requestedServerNames.length > 0) {
@@ -32145,6 +33087,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
32145
33087
  const agentFileChangeReportRequest = clientSupportsAgentFileChangeReports(this.clientCapabilities) ? parseAgentFileChangeReportRequest(params._meta) : null;
32146
33088
  let agentFileChangeReportTurnId = null;
32147
33089
  let agentFileChangeReportUnavailableReason = "providerError";
33090
+ let promptWasCancelled = false;
32148
33091
  let recoverableSessionFailure = sessionState.sessionFailure;
32149
33092
  sessionState.currentTurnId = null;
32150
33093
  sessionState.lastTokenUsage = null;
@@ -32168,6 +33111,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
32168
33111
  }
32169
33112
  };
32170
33113
  const cancelledPromptResponse = () => {
33114
+ promptWasCancelled = true;
32171
33115
  agentFileChangeReportTurnId = null;
32172
33116
  agentFileChangeReportUnavailableReason = "cancelled";
32173
33117
  return this.cancelledPromptResponse(sessionState);
@@ -32178,32 +33122,46 @@ Check ${configPath} and project .codex directories, especially their config.toml
32178
33122
  sessionState,
32179
33123
  clientSupportsPlanUpdates(this.clientCapabilities),
32180
33124
  clientSupportsTypedSessionFailures(this.clientCapabilities),
32181
- this.sessionFailureEpoch
33125
+ this.sessionFailureEpoch,
33126
+ sessionState.subagents
32182
33127
  );
32183
33128
  eventHandler = promptEventHandler;
32184
- const approvalHandler = new CodexApprovalHandler(this.connection, sessionState, activePrompt.signal);
33129
+ const permissionLifecycle = this.permissionLifecycleContext(sessionState);
33130
+ const permissionContext = permissionLifecycle.beginPrompt();
33131
+ const approvalHandler = new CodexApprovalHandler(
33132
+ this.connection,
33133
+ permissionContext,
33134
+ activePrompt.signal
33135
+ );
32185
33136
  const elicitationHandler = new CodexElicitationHandler(
32186
33137
  this.connection,
32187
- sessionState,
33138
+ permissionContext,
32188
33139
  this.clientCapabilities,
32189
33140
  activePrompt.signal
32190
33141
  );
33142
+ const observeInteraction = async (event) => {
33143
+ permissionContext.handleNotification(event);
33144
+ await elicitationHandler.handleNotification(event);
33145
+ };
32191
33146
  await this.codexAcpClient.subscribeToSessionEvents(
32192
33147
  params.sessionId,
32193
33148
  async (event) => {
33149
+ await observeInteraction(event);
32194
33150
  if (!promptNotificationsActive) {
32195
33151
  await promptEventHandler.handleSessionScopedNotification(event);
32196
33152
  return;
32197
33153
  }
32198
- const completesActiveTurn = event.method === "turn/completed" && event.params.turn.id === sessionState.currentTurnId;
32199
- await elicitationHandler.handleNotification(event);
33154
+ const completesActiveTurn = event.method === "turn/completed" && event.params.threadId === sessionState.sessionId && event.params.turn.id === sessionState.currentTurnId;
32200
33155
  await promptEventHandler.handleNotification(event);
32201
33156
  if (completesActiveTurn) {
32202
33157
  promptNotificationsActive = false;
32203
33158
  }
32204
33159
  },
32205
33160
  approvalHandler,
32206
- elicitationHandler
33161
+ elicitationHandler,
33162
+ clientSupportsSubagents(this.clientCapabilities),
33163
+ observeInteraction,
33164
+ (childThreadId) => promptEventHandler.waitForNativeSubagentSession(childThreadId)
32207
33165
  );
32208
33166
  if (activePrompt.signal.aborted) {
32209
33167
  return cancelledPromptResponse();
@@ -32344,6 +33302,15 @@ Check ${configPath} and project .codex directories, especially their config.toml
32344
33302
  return cancelledPromptResponse();
32345
33303
  }
32346
33304
  await this.codexAcpClient.waitForSessionNotifications(params.sessionId);
33305
+ if (turnCompleted.turn.status === "completed") {
33306
+ await eventHandler.waitForNativeSubagents(activePrompt.signal);
33307
+ if (activePrompt.signal.aborted) return cancelledPromptResponse();
33308
+ await this.codexAcpClient.waitForSessionNotifications(params.sessionId);
33309
+ } else {
33310
+ await eventHandler.finishOutstandingNativeSubagents(
33311
+ turnCompleted.turn.status === "interrupted" ? "cancelled" : "failed"
33312
+ );
33313
+ }
32347
33314
  await eventHandler.flushPendingErrors();
32348
33315
  await eventHandler.handleFailedTurn(turnCompleted.turn);
32349
33316
  promptNotificationsActive = false;
@@ -32424,6 +33391,15 @@ Check ${configPath} and project .codex directories, especially their config.toml
32424
33391
  return cancelledPromptResponse();
32425
33392
  }
32426
33393
  await this.codexAcpClient.waitForSessionNotifications(params.sessionId);
33394
+ if (turnCompleted.turn.status === "completed") {
33395
+ await eventHandler.waitForNativeSubagents(activePrompt.signal);
33396
+ if (activePrompt.signal.aborted) return cancelledPromptResponse();
33397
+ await this.codexAcpClient.waitForSessionNotifications(params.sessionId);
33398
+ } else {
33399
+ await eventHandler.finishOutstandingNativeSubagents(
33400
+ turnCompleted.turn.status === "interrupted" ? "cancelled" : "failed"
33401
+ );
33402
+ }
32427
33403
  await eventHandler.flushPendingErrors();
32428
33404
  await eventHandler.handleFailedTurn(turnCompleted.turn);
32429
33405
  promptNotificationsActive = false;
@@ -32485,6 +33461,14 @@ Check ${configPath} and project .codex directories, especially their config.toml
32485
33461
  throw err;
32486
33462
  } finally {
32487
33463
  promptNotificationsActive = false;
33464
+ try {
33465
+ await this.codexAcpClient.waitForSessionNotifications(params.sessionId);
33466
+ await eventHandler?.finishOutstandingNativeSubagents(
33467
+ promptWasCancelled || activePrompt.signal.aborted || this.sessionIsClosing(params.sessionId) ? "cancelled" : "failed"
33468
+ );
33469
+ } catch (error51) {
33470
+ logger.error("Failed to publish terminal subagent state during prompt cleanup", error51);
33471
+ }
32488
33472
  if (agentFileChangeReportRequest !== null) {
32489
33473
  await this.publishAgentFileChangeReport(
32490
33474
  sessionState,
@@ -32507,41 +33491,14 @@ Check ${configPath} and project .codex directories, especially their config.toml
32507
33491
  }
32508
33492
  }
32509
33493
  async requestPlanImplementationPermission(sessionState, plan, cancellationSignal) {
32510
- const toolCallId = `plan-review:${plan.itemId}`;
33494
+ const toolCallId = planImplementationToolCallId(plan);
32511
33495
  try {
32512
33496
  const response = await this.connection.request(
32513
33497
  methods.client.session.requestPermission,
32514
- {
32515
- sessionId: sessionState.sessionId,
32516
- toolCall: {
32517
- toolCallId,
32518
- title: "Implement this plan?",
32519
- kind: "switch_mode",
32520
- status: "pending",
32521
- rawInput: { plan: plan.text }
32522
- },
32523
- options: [
32524
- {
32525
- optionId: IMPLEMENT_PLAN_OPTION_ID,
32526
- name: "Yes, implement this plan",
32527
- kind: "allow_once"
32528
- },
32529
- {
32530
- optionId: REVISE_PLAN_OPTION_ID,
32531
- name: "No, and tell Codex what to do differently",
32532
- kind: "reject_once"
32533
- }
32534
- ],
32535
- _meta: {
32536
- codex: {
32537
- kind: "plan_review",
32538
- planItemId: plan.itemId
32539
- }
32540
- }
32541
- },
33498
+ planImplementationPermissionRequest(sessionState.sessionId, plan),
32542
33499
  { cancellationSignal }
32543
33500
  );
32544
- const approved = response.outcome.outcome === "selected" && response.outcome.optionId === IMPLEMENT_PLAN_OPTION_ID;
33501
+ const approved = planImplementationApproved(response);
32545
33502
  await this.connection.notify(methods.client.session.update, {
32546
33503
  sessionId: sessionState.sessionId,
32547
33504
  update: {