lody 0.74.0 → 0.76.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.
@@ -17519,6 +17519,7 @@ var zToolCallUpdate = object({
17519
17519
  kind: defaultOnError(zToolKind.nullish(), () => void 0),
17520
17520
  status: defaultOnError(zToolCallStatus.nullish(), () => void 0),
17521
17521
  title: defaultOnError(string2().nullish(), () => void 0),
17522
+ name: defaultOnError(string2().nullish(), () => void 0),
17522
17523
  content: defaultOnError(vecSkipError(zToolCallContent).nullish(), () => void 0),
17523
17524
  locations: defaultOnError(vecSkipError(zToolCallLocation).nullish(), () => void 0),
17524
17525
  rawInput: defaultOnError(unknown().optional(), () => void 0),
@@ -18284,6 +18285,7 @@ var zContentChunk = object({
18284
18285
  var zToolCall = object({
18285
18286
  toolCallId: zToolCallId,
18286
18287
  title: string2(),
18288
+ name: defaultOnError(string2().nullish(), () => void 0),
18287
18289
  kind: defaultOnError(zToolKind.optional(), () => void 0),
18288
18290
  status: defaultOnError(zToolCallStatus.optional(), () => void 0),
18289
18291
  content: defaultOnError(vecSkipError(zToolCallContent).optional(), () => []),
@@ -18964,63 +18966,70 @@ object({
18964
18966
  requestId: zRequestId,
18965
18967
  _meta: defaultOnError(record(string2(), unknown()).nullish(), () => void 0)
18966
18968
  });
18967
- function tagOf(value, key) {
18968
- return typeof value === "object" && value !== null ? value[key] : void 0;
18969
+ var CANCEL_REQUEST_METHOD = "$/cancel_request";
18970
+ function isRequestMessage(value) {
18971
+ return isJsonRpcEnvelope(value) && "id" in value && typeof value["method"] === "string" && isJsonRpcId(value["id"]);
18969
18972
  }
18970
- zElicitationFormMode.and(object({ mode: literal("form") })).and(object({ message: string2() }));
18971
- zElicitationUrlMode.and(object({ mode: literal("url") })).and(object({ message: string2() }));
18972
- union([zElicitationSessionScope, zElicitationRequestScope]).and(object({ message: string2() }));
18973
- zStringPropertySchema.and(object({ type: literal("string") }));
18974
- zNumberPropertySchema.and(object({ type: literal("number") }));
18975
- zIntegerPropertySchema.and(object({ type: literal("integer") }));
18976
- zBooleanPropertySchema.and(object({ type: literal("boolean") }));
18977
- zMultiSelectPropertySchema.and(object({ type: literal("array") }));
18978
- zStringMultiSelectItems.and(object({ type: literal("string") }));
18979
- var zGuardCreateElicitationResponseAccept = zElicitationAcceptAction.and(object({ action: literal("accept") }));
18980
- var zGuardCreateElicitationResponseDecline = object({
18981
- action: literal("decline")
18982
- });
18983
- var zGuardCreateElicitationResponseCancel = object({
18984
- action: literal("cancel")
18985
- });
18986
- var CreateElicitationResponse = {
18987
- /** Narrow to the `accept` variant, validating its payload. */
18988
- isAccept(value) {
18989
- return tagOf(value, "action") === "accept" && zGuardCreateElicitationResponseAccept.safeParse(value).success;
18990
- },
18991
- /** Narrow to the `decline` variant, validating its payload. */
18992
- isDecline(value) {
18993
- return tagOf(value, "action") === "decline" && zGuardCreateElicitationResponseDecline.safeParse(value).success;
18994
- },
18995
- /** Narrow to the `cancel` variant, validating its payload. */
18996
- isCancel(value) {
18997
- return tagOf(value, "action") === "cancel" && zGuardCreateElicitationResponseCancel.safeParse(value).success;
18998
- },
18999
- /**
19000
- * Narrow to a custom or future variant: the `action` tag matches no known variant.
19001
- *
19002
- * TypeScript keeps the known variants in the narrowed union (they are
19003
- * structural subtypes of the catch-all), so read vendor payload keys
19004
- * via a widening cast: `(value as Record<string, unknown>).someKey`.
19005
- */
19006
- isCustom(value) {
19007
- const tag = tagOf(value, "action");
19008
- return typeof tag === "string" && !["accept", "cancel", "decline"].includes(tag);
18973
+ function isResponseMessage(value) {
18974
+ if (!isJsonRpcEnvelope(value) || "method" in value) {
18975
+ return false;
19009
18976
  }
19010
- };
19011
- var CANCEL_REQUEST_METHOD = "$/cancel_request";
18977
+ if (!("id" in value) || !isJsonRpcId(value["id"])) {
18978
+ return false;
18979
+ }
18980
+ const hasResult = Object.hasOwn(value, "result");
18981
+ const hasError = Object.hasOwn(value, "error");
18982
+ if (hasResult === hasError) {
18983
+ return false;
18984
+ }
18985
+ return !hasError || isErrorResponse(value["error"]);
18986
+ }
18987
+ function isNotificationMessage(value) {
18988
+ return isJsonRpcEnvelope(value) && !("id" in value) && typeof value["method"] === "string";
18989
+ }
19012
18990
  function isRecord(value) {
19013
18991
  return typeof value === "object" && value !== null;
19014
18992
  }
18993
+ function isJsonRpcEnvelope(value) {
18994
+ return isRecord(value) && value["jsonrpc"] === "2.0";
18995
+ }
19015
18996
  function isJsonRpcId(value) {
19016
18997
  return value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value);
19017
18998
  }
18999
+ function isResponseShapedMessage(value) {
19000
+ return isRecord(value) && !("method" in value) && ("id" in value || "result" in value || "error" in value);
19001
+ }
19002
+ function isResponseBatch(batch) {
19003
+ let hasValidCall = false;
19004
+ let hasValidResponse = false;
19005
+ let hasCallShape = false;
19006
+ let hasResponseShape = false;
19007
+ for (const entry of batch) {
19008
+ hasValidCall ||= isRequestMessage(entry) || isNotificationMessage(entry);
19009
+ hasValidResponse ||= isResponseMessage(entry);
19010
+ if (!isRecord(entry)) {
19011
+ continue;
19012
+ }
19013
+ hasCallShape ||= "method" in entry;
19014
+ hasResponseShape ||= "result" in entry || "error" in entry;
19015
+ }
19016
+ if (hasValidCall) {
19017
+ return false;
19018
+ }
19019
+ if (hasValidResponse) {
19020
+ return true;
19021
+ }
19022
+ return hasResponseShape && !hasCallShape;
19023
+ }
19018
19024
  function cancelRequestId(params) {
19019
19025
  if (!isRecord(params) || !isJsonRpcId(params["requestId"])) {
19020
19026
  return void 0;
19021
19027
  }
19022
19028
  return params["requestId"];
19023
19029
  }
19030
+ function isErrorResponse(value) {
19031
+ return isRecord(value) && typeof value["code"] === "number" && Number.isInteger(value["code"]) && typeof value["message"] === "string";
19032
+ }
19024
19033
  var Handled = {
19025
19034
  /**
19026
19035
  * Marks a message as handled.
@@ -19180,6 +19189,12 @@ var ConnectionContext = class {
19180
19189
  sendNotification(method, params) {
19181
19190
  return this.connection.sendNotification(method, params);
19182
19191
  }
19192
+ /**
19193
+ * Sends a non-empty JSON-RPC batch in one transport message.
19194
+ */
19195
+ sendBatch(entries) {
19196
+ return this.connection.sendBatch(entries);
19197
+ }
19183
19198
  /**
19184
19199
  * Sends a protocol-level request cancellation notification.
19185
19200
  */
@@ -19218,6 +19233,7 @@ var Connection = class {
19218
19233
  retryQueue = [];
19219
19234
  context = new ConnectionContext(this);
19220
19235
  receiveReader;
19236
+ allowBatches = true;
19221
19237
  constructor(requestHandlerOrStream, notificationHandlerOrHandlers, streamOrOptions, options) {
19222
19238
  if (typeof requestHandlerOrStream === "function") {
19223
19239
  const requestHandler = requestHandlerOrStream;
@@ -19226,16 +19242,13 @@ var Connection = class {
19226
19242
  this.initialize(stream2, [
19227
19243
  ...options?.handlers ?? [],
19228
19244
  this.legacyHandler(requestHandler, notificationHandler)
19229
- ]);
19245
+ ], options);
19230
19246
  return;
19231
19247
  }
19232
19248
  const stream = requestHandlerOrStream;
19233
19249
  const handlers = notificationHandlerOrHandlers;
19234
19250
  const connectionOptions = streamOrOptions;
19235
- this.initialize(stream, [
19236
- ...connectionOptions?.handlers ?? [],
19237
- ...handlers
19238
- ]);
19251
+ this.initialize(stream, [...connectionOptions?.handlers ?? [], ...handlers], connectionOptions);
19239
19252
  }
19240
19253
  /**
19241
19254
  * Creates a builder for configuring a handler-based connection.
@@ -19309,15 +19322,89 @@ var Connection = class {
19309
19322
  if (this.abortController.signal.aborted) {
19310
19323
  return rejectedPromise(this.closedReason());
19311
19324
  }
19325
+ const request = this.prepareRequest(method, params, mapResponse, options);
19326
+ const requestSent = this.sendWireMessage(request.message);
19327
+ void requestSent.catch(() => {
19328
+ });
19329
+ if (options.cancellationSignal?.aborted) {
19330
+ request.cancel();
19331
+ }
19332
+ return request.response;
19333
+ }
19334
+ /**
19335
+ * Sends a non-empty JSON-RPC batch in one transport message.
19336
+ *
19337
+ * Requests and notifications are processed independently by the peer. The
19338
+ * returned tuple preserves the input order: request entries resolve to their
19339
+ * mapped response, while notification entries resolve to `undefined`.
19340
+ */
19341
+ sendBatch(entries) {
19342
+ if (this.abortController.signal.aborted) {
19343
+ return rejectedPromise(this.closedReason());
19344
+ }
19345
+ if (!this.allowBatches) {
19346
+ return rejectedPromise(new TypeError("JSON-RPC batches are not supported on this connection"));
19347
+ }
19348
+ if (entries.length === 0) {
19349
+ return rejectedPromise(new TypeError("JSON-RPC batch must contain at least one entry"));
19350
+ }
19351
+ const messages = [];
19352
+ const cancellations = [];
19353
+ const outputs = [];
19354
+ for (const entry of entries) {
19355
+ if (entry.kind === "notification") {
19356
+ messages.push({
19357
+ jsonrpc: "2.0",
19358
+ method: entry.method,
19359
+ params: entry.params
19360
+ });
19361
+ outputs.push(Promise.resolve(void 0));
19362
+ continue;
19363
+ }
19364
+ const request = this.prepareRequest(entry.method, entry.params, entry.mapResponse, entry.options);
19365
+ messages.push(request.message);
19366
+ outputs.push(request.response);
19367
+ cancellations.push({
19368
+ signal: entry.options?.cancellationSignal,
19369
+ cancel: request.cancel
19370
+ });
19371
+ }
19372
+ const batch = messages;
19373
+ const batchSent = this.sendWireMessage(batch);
19374
+ for (const cancellation of cancellations) {
19375
+ if (cancellation.signal?.aborted) {
19376
+ cancellation.cancel();
19377
+ }
19378
+ }
19379
+ const response = Promise.all([batchSent, ...outputs]).then(([, ...resolved]) => resolved);
19380
+ response.catch(() => {
19381
+ });
19382
+ return response;
19383
+ }
19384
+ /**
19385
+ * Sends a protocol-level request cancellation notification.
19386
+ */
19387
+ sendCancelRequest(requestId) {
19388
+ return this.sendNotification(CANCEL_REQUEST_METHOD, { requestId });
19389
+ }
19390
+ /**
19391
+ * Sends a JSON-RPC notification.
19392
+ */
19393
+ sendNotification(method, params) {
19394
+ if (this.abortController.signal.aborted) {
19395
+ return rejectedPromise(this.closedReason());
19396
+ }
19397
+ return this.sendWireMessage({ jsonrpc: "2.0", method, params });
19398
+ }
19399
+ prepareRequest(method, params, mapResponse, options = {}) {
19312
19400
  const id = this.nextRequestId++;
19313
19401
  let cancel = () => {
19314
19402
  };
19315
- const responsePromise = new Promise((resolve, reject) => {
19403
+ const response = new Promise((resolve, reject) => {
19316
19404
  const pendingResponse = {
19317
- resolve: (response) => {
19405
+ resolve: (value) => {
19318
19406
  try {
19319
- const value = mapResponse ? mapResponse(response) : response;
19320
- resolve(value);
19407
+ resolve(mapResponse ? mapResponse(value) : value);
19321
19408
  } catch (error48) {
19322
19409
  reject(error48);
19323
19410
  }
@@ -19341,35 +19428,13 @@ var Connection = class {
19341
19428
  };
19342
19429
  this.pendingResponses.set(id, pendingResponse);
19343
19430
  });
19344
- responsePromise.catch(() => {
19431
+ response.catch(() => {
19345
19432
  });
19346
- const requestSent = this.sendMessage({
19347
- jsonrpc: "2.0",
19348
- id,
19349
- method,
19350
- params
19351
- });
19352
- void requestSent.catch(() => {
19353
- });
19354
- if (options.cancellationSignal?.aborted) {
19355
- cancel();
19356
- }
19357
- return responsePromise;
19358
- }
19359
- /**
19360
- * Sends a protocol-level request cancellation notification.
19361
- */
19362
- sendCancelRequest(requestId) {
19363
- return this.sendNotification(CANCEL_REQUEST_METHOD, { requestId });
19364
- }
19365
- /**
19366
- * Sends a JSON-RPC notification.
19367
- */
19368
- sendNotification(method, params) {
19369
- if (this.abortController.signal.aborted) {
19370
- return rejectedPromise(this.closedReason());
19371
- }
19372
- return this.sendMessage({ jsonrpc: "2.0", method, params });
19433
+ return {
19434
+ message: { jsonrpc: "2.0", id, method, params },
19435
+ response,
19436
+ cancel: () => cancel()
19437
+ };
19373
19438
  }
19374
19439
  /**
19375
19440
  * Closes the connection and rejects pending requests.
@@ -19392,9 +19457,10 @@ var Connection = class {
19392
19457
  void this.receiveReader?.cancel(closeError).catch(() => {
19393
19458
  });
19394
19459
  }
19395
- initialize(stream, handlers) {
19460
+ initialize(stream, handlers, options) {
19396
19461
  this.stream = stream;
19397
19462
  this.staticHandlers = handlers;
19463
+ this.allowBatches = options?.allowBatches ?? true;
19398
19464
  this.closedPromise = new Promise((resolve) => {
19399
19465
  this.abortController.signal.addEventListener("abort", () => resolve());
19400
19466
  });
@@ -19430,7 +19496,7 @@ var Connection = class {
19430
19496
  if (!message) {
19431
19497
  continue;
19432
19498
  }
19433
- this.receiveMessage(message);
19499
+ this.receiveWireMessage(message);
19434
19500
  }
19435
19501
  } finally {
19436
19502
  if (this.receiveReader === reader) {
@@ -19444,24 +19510,93 @@ var Connection = class {
19444
19510
  this.close(closeError);
19445
19511
  }
19446
19512
  }
19447
- receiveMessage(message) {
19448
- if (this.abortController.signal.aborted) {
19513
+ receiveWireMessage(message) {
19514
+ if (Array.isArray(message)) {
19515
+ if (!this.allowBatches) {
19516
+ this.close(new TypeError("JSON-RPC batches are not supported on this connection"));
19517
+ return;
19518
+ }
19519
+ this.receiveBatch(message);
19449
19520
  return;
19450
19521
  }
19451
19522
  if (!isRecord(message)) {
19452
19523
  console.error("Invalid message", { message });
19453
19524
  return;
19454
19525
  }
19526
+ this.receiveMessage(message);
19527
+ }
19528
+ receiveBatch(batch) {
19529
+ if (batch.length === 0) {
19530
+ void this.sendWireMessage({
19531
+ jsonrpc: "2.0",
19532
+ id: null,
19533
+ error: RequestError.invalidRequest(batch).toErrorResponse()
19534
+ }).catch(() => {
19535
+ });
19536
+ return;
19537
+ }
19538
+ const responseBatch = isResponseBatch(batch);
19539
+ const responseCount = responseBatch ? 0 : batch.reduce((count, message) => count + (isNotificationMessage(message) ? 0 : 1), 0);
19540
+ let remaining = responseCount;
19541
+ let remainingNotifications = batch.reduce((count, message) => count + (isNotificationMessage(message) ? 1 : 0), 0);
19542
+ let responseSent = false;
19543
+ const responses = [];
19544
+ const sendResponsesIfReady = async () => {
19545
+ if (responseSent || remaining !== 0 || remainingNotifications !== 0 || responses.length === 0) {
19546
+ return;
19547
+ }
19548
+ responseSent = true;
19549
+ await this.sendWireMessage(responses);
19550
+ };
19551
+ const collectResponse = async (response) => {
19552
+ responses.push(response);
19553
+ remaining -= 1;
19554
+ await sendResponsesIfReady();
19555
+ };
19556
+ for (const message of batch) {
19557
+ if (responseBatch) {
19558
+ if (isResponseShapedMessage(message)) {
19559
+ this.receiveMessage(message);
19560
+ }
19561
+ continue;
19562
+ }
19563
+ if (!isRequestMessage(message) && !isNotificationMessage(message)) {
19564
+ void collectResponse({
19565
+ jsonrpc: "2.0",
19566
+ id: null,
19567
+ error: RequestError.invalidRequest(message).toErrorResponse()
19568
+ }).catch(() => {
19569
+ });
19570
+ continue;
19571
+ }
19572
+ const processing = this.receiveMessage(message, isRequestMessage(message) ? collectResponse : void 0);
19573
+ if (isNotificationMessage(message)) {
19574
+ void processing.finally(() => {
19575
+ remainingNotifications -= 1;
19576
+ void sendResponsesIfReady().catch((error48) => this.close(error48));
19577
+ });
19578
+ }
19579
+ }
19580
+ }
19581
+ receiveMessage(message, sendResponse) {
19582
+ if (this.abortController.signal.aborted) {
19583
+ return Promise.resolve();
19584
+ }
19585
+ if (!isRecord(message)) {
19586
+ console.error("Invalid message", { message });
19587
+ return Promise.resolve();
19588
+ }
19455
19589
  if ("method" in message) {
19456
19590
  if (!("id" in message)) {
19457
19591
  this.handleProtocolNotification(message);
19458
19592
  }
19459
- void this.processIncomingMessage(this.toIncomingMessage(message)).catch((error48) => this.close(error48));
19593
+ return this.processIncomingMessage(this.toIncomingMessage(message, sendResponse)).catch((error48) => this.close(error48));
19460
19594
  } else if ("id" in message) {
19461
19595
  this.handleResponse(message);
19462
19596
  } else {
19463
19597
  console.error("Invalid message", { message });
19464
19598
  }
19599
+ return Promise.resolve();
19465
19600
  }
19466
19601
  async processIncomingMessage(message) {
19467
19602
  if (this.abortController.signal.aborted) {
@@ -19505,7 +19640,7 @@ var Connection = class {
19505
19640
  }
19506
19641
  }
19507
19642
  }
19508
- toIncomingMessage(message) {
19643
+ toIncomingMessage(message, sendResponse) {
19509
19644
  if ("id" in message) {
19510
19645
  const abortController = new AbortController();
19511
19646
  this.incomingRequests.set(message.id, abortController);
@@ -19520,11 +19655,14 @@ var Connection = class {
19520
19655
  params: message.params,
19521
19656
  raw: message,
19522
19657
  signal: abortController.signal,
19523
- responder: new RequestResponder(message.id, (result) => this.sendMessage({
19524
- jsonrpc: "2.0",
19525
- id: message.id,
19526
- ...result
19527
- }), abortController.signal, finishRequest)
19658
+ responder: new RequestResponder(message.id, (result) => {
19659
+ const response = {
19660
+ jsonrpc: "2.0",
19661
+ id: message.id,
19662
+ ...result
19663
+ };
19664
+ return sendResponse ? sendResponse(response) : this.sendWireMessage(response);
19665
+ }, abortController.signal, finishRequest)
19528
19666
  };
19529
19667
  }
19530
19668
  return {
@@ -19539,13 +19677,13 @@ var Connection = class {
19539
19677
  if (pendingResponse) {
19540
19678
  this.pendingResponses.delete(response.id);
19541
19679
  pendingResponse.cleanup?.();
19542
- if ("result" in response) {
19680
+ if (!isResponseMessage(response)) {
19681
+ pendingResponse.reject(RequestError.invalidRequest(response));
19682
+ } else if ("result" in response) {
19543
19683
  pendingResponse.resolve(response.result);
19544
- } else if ("error" in response && isRecord(response.error)) {
19684
+ } else {
19545
19685
  const { code, message, data } = response.error;
19546
19686
  pendingResponse.reject(new RequestError(code, message, data));
19547
- } else {
19548
- pendingResponse.reject(RequestError.invalidRequest(response));
19549
19687
  }
19550
19688
  } else {
19551
19689
  console.error("Got response to unknown request", response.id);
@@ -19568,7 +19706,7 @@ var Connection = class {
19568
19706
  closedReason() {
19569
19707
  return this.abortController.signal.reason ?? new Error("ACP connection closed");
19570
19708
  }
19571
- async sendMessage(message) {
19709
+ async sendWireMessage(message) {
19572
19710
  if (this.abortController.signal.aborted) {
19573
19711
  return rejectedPromise(this.closedReason());
19574
19712
  }
@@ -19810,7 +19948,7 @@ function ndJsonStream(output, input) {
19810
19948
  if (trimmedLine) {
19811
19949
  try {
19812
19950
  const message = JSON.parse(trimmedLine);
19813
- if (isRecord(message)) {
19951
+ if (isRecord(message) || Array.isArray(message)) {
19814
19952
  controller.enqueue(message);
19815
19953
  } else {
19816
19954
  console.warn("Skipping JSON line that is not an object:", trimmedLine);
@@ -19883,6 +20021,53 @@ function ndJsonStream(output, input) {
19883
20021
  });
19884
20022
  return { readable, writable };
19885
20023
  }
20024
+ function tagOf(value, key) {
20025
+ return typeof value === "object" && value !== null ? value[key] : void 0;
20026
+ }
20027
+ zElicitationFormMode.and(object({ mode: literal("form") })).and(object({ message: string2() }));
20028
+ zElicitationUrlMode.and(object({ mode: literal("url") })).and(object({ message: string2() }));
20029
+ union([zElicitationSessionScope, zElicitationRequestScope]).and(object({ message: string2() }));
20030
+ zStringPropertySchema.and(object({ type: literal("string") }));
20031
+ zNumberPropertySchema.and(object({ type: literal("number") }));
20032
+ zIntegerPropertySchema.and(object({ type: literal("integer") }));
20033
+ zBooleanPropertySchema.and(object({ type: literal("boolean") }));
20034
+ zMultiSelectPropertySchema.and(object({ type: literal("array") }));
20035
+ zStringMultiSelectItems.and(object({ type: literal("string") }));
20036
+ var zGuardCreateElicitationResponseAccept = zElicitationAcceptAction.and(object({ action: literal("accept") }));
20037
+ var zGuardCreateElicitationResponseDecline = object({
20038
+ action: literal("decline")
20039
+ });
20040
+ var zGuardCreateElicitationResponseCancel = object({
20041
+ action: literal("cancel")
20042
+ });
20043
+ var CreateElicitationResponse = {
20044
+ /** Narrow to the `accept` variant, validating its payload. */
20045
+ isAccept(value) {
20046
+ return tagOf(value, "action") === "accept" && zGuardCreateElicitationResponseAccept.safeParse(value).success;
20047
+ },
20048
+ /** Narrow to the `decline` variant, validating its payload. */
20049
+ isDecline(value) {
20050
+ return tagOf(value, "action") === "decline" && zGuardCreateElicitationResponseDecline.safeParse(value).success;
20051
+ },
20052
+ /** Narrow to the `cancel` variant, validating its payload. */
20053
+ isCancel(value) {
20054
+ return tagOf(value, "action") === "cancel" && zGuardCreateElicitationResponseCancel.safeParse(value).success;
20055
+ },
20056
+ /**
20057
+ * Narrow to a custom or future variant: the `action` tag matches no known variant.
20058
+ *
20059
+ * TypeScript keeps the known variants in the narrowed union (they are
20060
+ * structural subtypes of the catch-all), so read vendor payload keys
20061
+ * via a widening cast: `(value as Record<string, unknown>).someKey`.
20062
+ */
20063
+ isCustom(value) {
20064
+ const tag = tagOf(value, "action");
20065
+ return typeof tag === "string" && !["accept", "cancel", "decline"].includes(tag);
20066
+ }
20067
+ };
20068
+ function ndJsonStream2(output, input) {
20069
+ return ndJsonStream(output, input);
20070
+ }
19886
20071
  function emptyObjectResponse(response) {
19887
20072
  return response ?? {};
19888
20073
  }
@@ -20529,6 +20714,7 @@ function runConnectHandlers(connection, handlers) {
20529
20714
  var appBuilder = /* @__PURE__ */ Symbol("appBuilder");
20530
20715
  var runAgentConnectHandlers = /* @__PURE__ */ Symbol("runAgentConnectHandlers");
20531
20716
  var runClientConnectHandlers = /* @__PURE__ */ Symbol("runClientConnectHandlers");
20717
+ var stableConnectionOptions = { allowBatches: false };
20532
20718
  function agent(options) {
20533
20719
  return new AgentApp(options);
20534
20720
  }
@@ -20602,7 +20788,7 @@ var AgentApp = class {
20602
20788
  return state2;
20603
20789
  }
20604
20790
  const [thisStream, peerStream] = memoryStreamPair();
20605
- const peerRawConnection = target[appBuilder]().connect(peerStream);
20791
+ const peerRawConnection = target[appBuilder]().connect(peerStream, stableConnectionOptions);
20606
20792
  const peerConnection = clientConnection(peerRawConnection);
20607
20793
  const state = this.openStreamConnection(thisStream);
20608
20794
  void state.rawConnection.closed.then(() => peerConnection.close());
@@ -20618,7 +20804,7 @@ var AgentApp = class {
20618
20804
  return state;
20619
20805
  }
20620
20806
  openStreamConnection(stream) {
20621
- const rawConnection = this.builder.connect(stream);
20807
+ const rawConnection = this.builder.connect(stream, stableConnectionOptions);
20622
20808
  return {
20623
20809
  rawConnection,
20624
20810
  connection: agentConnection(rawConnection, this.connectHandlers)
@@ -20688,7 +20874,7 @@ function createJSONRPCReader(readable) {
20688
20874
  function createJsonStream(readable, writable) {
20689
20875
  const input = Writable.toWeb(writable);
20690
20876
  const output = Readable.toWeb(readable);
20691
- return ndJsonStream(input, output);
20877
+ return ndJsonStream2(input, output);
20692
20878
  }
20693
20879
  var Logger = class {
20694
20880
  logFilePath;
@@ -20804,6 +20990,8 @@ var ACP_EXT_SESSION_USAGE_UPDATE_METHOD = "_acp_ext:session_usage_update";
20804
20990
  var ACP_EXT_SESSION_RATE_LIMITS_METHOD = "_acp_ext:session_rate_limits";
20805
20991
  var ACP_EXT_CODEX_PROPOSED_PLAN_METHOD = "_acp_ext:codex_proposed_plan";
20806
20992
  var CODEX_STEER_APPLIED_METHOD = "_codex/steerApplied";
20993
+ var SESSION_STEERING_METHOD = "_session/steering";
20994
+ var GOAL_CONTROL_METHOD = "_codex/session/goal_control";
20807
20995
  function getLodyForkTurnId(meta3) {
20808
20996
  if (typeof meta3 !== "object" || meta3 === null) return null;
20809
20997
  const lody = meta3["lody"];
@@ -20816,21 +21004,13 @@ function getLodyForkTurnId(meta3) {
20816
21004
  }
20817
21005
  var CODEX_STEER_CAPABILITY = {
20818
21006
  version: 1,
21007
+ method: SESSION_STEERING_METHOD,
20819
21008
  appliedNotification: CODEX_STEER_APPLIED_METHOD,
20820
21009
  upstreamTurn: "same",
20821
21010
  configPolicy: "active"
20822
21011
  };
20823
- function getCodexSteerId(meta3) {
20824
- if (typeof meta3 !== "object" || meta3 === null) return null;
20825
- const codex = meta3["codex"];
20826
- if (typeof codex !== "object" || codex === null) return null;
20827
- const steer = codex["steer"];
20828
- if (typeof steer !== "object" || steer === null) return null;
20829
- const id = steer["id"];
20830
- return typeof id === "string" && id.length > 0 ? id : null;
20831
- }
20832
21012
  function isExtMethodRequest(request) {
20833
- return request.method === "authentication/status" || request.method === "authentication/logout" || request.method === LEGACY_SET_SESSION_MODEL_METHOD;
21013
+ return request.method === "authentication/status" || request.method === "authentication/logout" || request.method === LEGACY_SET_SESSION_MODEL_METHOD || request.method === GOAL_CONTROL_METHOD || request.method === SESSION_STEERING_METHOD;
20834
21014
  }
20835
21015
  function toTokenCount(usage) {
20836
21016
  return {
@@ -21737,7 +21917,7 @@ function createWebSearchStartUpdate(item) {
21737
21917
  kind: "search",
21738
21918
  title: formatWebSearchTitle(item),
21739
21919
  status: "in_progress",
21740
- rawInput
21920
+ rawInput: createWebSearchRawInput(item)
21741
21921
  };
21742
21922
  }
21743
21923
  function createWebSearchCompleteUpdate(item) {
@@ -21747,7 +21927,15 @@ function createWebSearchCompleteUpdate(item) {
21747
21927
  toolCallId: item.id,
21748
21928
  title: formatWebSearchTitle(item),
21749
21929
  status: "completed",
21750
- rawInput
21930
+ rawInput: createWebSearchRawInput(item)
21931
+ };
21932
+ }
21933
+ function createWebSearchRawInput(item) {
21934
+ return {
21935
+ type: item.type,
21936
+ id: item.id,
21937
+ query: item.query,
21938
+ action: item.action
21751
21939
  };
21752
21940
  }
21753
21941
  function createCollabAgentToolCallUpdate(item) {
@@ -21757,7 +21945,8 @@ function createCollabAgentToolCallUpdate(item) {
21757
21945
  kind: "other",
21758
21946
  title: item.tool,
21759
21947
  status: toAcpStatus(item.status),
21760
- rawInput: createCollabAgentToolCallRawInput(item)
21948
+ rawInput: createCollabAgentToolCallRawInput(item),
21949
+ _meta: createCollabAgentToolCallMeta(item)
21761
21950
  };
21762
21951
  }
21763
21952
  function createCollabAgentToolCallCompleteUpdate(item) {
@@ -21766,7 +21955,8 @@ function createCollabAgentToolCallCompleteUpdate(item) {
21766
21955
  toolCallId: item.id,
21767
21956
  title: item.tool,
21768
21957
  status: toAcpStatus(item.status),
21769
- rawInput: createCollabAgentToolCallRawInput(item)
21958
+ rawInput: createCollabAgentToolCallRawInput(item),
21959
+ _meta: createCollabAgentToolCallMeta(item)
21770
21960
  };
21771
21961
  }
21772
21962
  function createCollabAgentToolCallRawInput(item) {
@@ -21775,9 +21965,66 @@ function createCollabAgentToolCallRawInput(item) {
21775
21965
  senderThreadId: item.senderThreadId,
21776
21966
  receiverThreadIds: item.receiverThreadIds,
21777
21967
  agentsStates: item.agentsStates,
21968
+ model: item.model,
21969
+ reasoningEffort: item.reasoningEffort,
21778
21970
  status: item.status
21779
21971
  };
21780
21972
  }
21973
+ function createCollabAgentToolCallMeta(item) {
21974
+ return {
21975
+ codex: {
21976
+ collaboration: {
21977
+ tool: item.tool,
21978
+ senderThreadId: item.senderThreadId,
21979
+ receiverThreadIds: item.receiverThreadIds
21980
+ }
21981
+ }
21982
+ };
21983
+ }
21984
+ function createSubAgentActivityUpdate(item, status, sessionUpdate) {
21985
+ const name = item.agentPath.split("/").filter(Boolean).at(-1) ?? "subagent";
21986
+ const title = formatSubAgentActivityTitle(item.kind, name);
21987
+ const common = {
21988
+ toolCallId: item.id,
21989
+ status,
21990
+ rawInput: {
21991
+ agentThreadId: item.agentThreadId,
21992
+ agentPath: item.agentPath,
21993
+ activityKind: item.kind
21994
+ },
21995
+ _meta: {
21996
+ codex: {
21997
+ subagent: {
21998
+ threadId: item.agentThreadId,
21999
+ path: item.agentPath,
22000
+ activity: item.kind
22001
+ }
22002
+ }
22003
+ }
22004
+ };
22005
+ if (sessionUpdate === "tool_call") {
22006
+ return {
22007
+ sessionUpdate,
22008
+ title,
22009
+ kind: "other",
22010
+ ...common
22011
+ };
22012
+ }
22013
+ return {
22014
+ sessionUpdate,
22015
+ ...common
22016
+ };
22017
+ }
22018
+ function formatSubAgentActivityTitle(kind, name) {
22019
+ switch (kind) {
22020
+ case "started":
22021
+ return `Start subagent ${name}`;
22022
+ case "interacted":
22023
+ return `Interact with subagent ${name}`;
22024
+ case "interrupted":
22025
+ return `Interrupt subagent ${name}`;
22026
+ }
22027
+ }
21781
22028
  function formatWebSearchTitle(item) {
21782
22029
  const action = item.action;
21783
22030
  if (!action) {
@@ -22249,14 +22496,39 @@ function createAgentTextMessageChunk(text, messageId, meta3) {
22249
22496
  function createAgentTextThoughtChunk(text, messageId, meta3) {
22250
22497
  return createAgentThoughtChunk({ type: "text", text }, messageId);
22251
22498
  }
22252
- var CodexEventHandler = class {
22499
+ function toThreadGoalSnapshot(goal) {
22500
+ return {
22501
+ objective: goal.objective.trim(),
22502
+ status: goal.status,
22503
+ tokenBudget: goal.tokenBudget,
22504
+ timeUsedSeconds: goal.timeUsedSeconds,
22505
+ createdAt: goal.createdAt,
22506
+ controlMethod: GOAL_CONTROL_METHOD
22507
+ };
22508
+ }
22509
+ function sameThreadGoalSnapshot(left, right) {
22510
+ if (left === void 0) return false;
22511
+ if (left === null || right === null) return left === right;
22512
+ return left.objective === right.objective && left.status === right.status && left.tokenBudget === right.tokenBudget && left.createdAt === right.createdAt;
22513
+ }
22514
+ var CodexEventHandler = class _CodexEventHandler {
22515
+ static PLAN_UPDATE_INTERVAL_MS = 150;
22253
22516
  connection;
22254
22517
  sessionState;
22518
+ supportsPlanUpdates;
22255
22519
  failure = null;
22520
+ completedPlan = null;
22256
22521
  activeFuzzyFileSearchSessions = /* @__PURE__ */ new Set();
22257
22522
  activeGuardianApprovalReviews = /* @__PURE__ */ new Set();
22258
22523
  activeImageGenerationItems = /* @__PURE__ */ new Set();
22259
22524
  emittedImageViewItems = /* @__PURE__ */ new Set();
22525
+ planDeltaTextByItemId = /* @__PURE__ */ new Map();
22526
+ pendingPlanItemIds = /* @__PURE__ */ new Set();
22527
+ lastEmittedPlanTextByItemId = /* @__PURE__ */ new Map();
22528
+ session;
22529
+ planUpdateTimer = null;
22530
+ planUpdateChain = Promise.resolve();
22531
+ disposed = false;
22260
22532
  seenReasoningDeltaItemIds = /* @__PURE__ */ new Set();
22261
22533
  reasoningSummaryFilters = /* @__PURE__ */ new Map();
22262
22534
  terminalCommandIds = /* @__PURE__ */ new Set();
@@ -22264,29 +22536,59 @@ var CodexEventHandler = class {
22264
22536
  proposedPlanMarkdown = "";
22265
22537
  proposedPlanTurnId = null;
22266
22538
  agentMessagePhases = /* @__PURE__ */ new Map();
22267
- constructor(connection, sessionState) {
22539
+ activeSubAgentActivities = /* @__PURE__ */ new Set();
22540
+ constructor(connection, sessionState, supportsPlanUpdates = false) {
22268
22541
  this.connection = connection;
22269
22542
  this.sessionState = sessionState;
22543
+ this.supportsPlanUpdates = supportsPlanUpdates;
22544
+ this.session = new ACPSessionConnection(connection, sessionState.sessionId);
22270
22545
  }
22271
22546
  getFailure() {
22272
22547
  return this.failure;
22273
22548
  }
22549
+ takeCompletedPlan() {
22550
+ const plan = this.completedPlan;
22551
+ this.completedPlan = null;
22552
+ return plan;
22553
+ }
22274
22554
  async handleNotification(notification) {
22275
22555
  if (notification.method === "account/rateLimits/updated") {
22276
22556
  await this.handleRateLimitsSnapshot(notification.params.rateLimits, true);
22277
22557
  return;
22278
22558
  }
22279
- const session = new ACPSessionConnection(this.connection, this.sessionState.sessionId);
22280
22559
  const updateEvent = await this.createUpdateEvent(notification);
22281
22560
  if (updateEvent) {
22282
- await session.update(updateEvent);
22561
+ await this.session.update(updateEvent);
22283
22562
  }
22284
22563
  await this.emitExtNotification(notification);
22285
22564
  }
22565
+ async flushPendingPlanUpdates() {
22566
+ this.cancelPlanUpdateTimer();
22567
+ do {
22568
+ const itemIds = [...this.pendingPlanItemIds];
22569
+ this.pendingPlanItemIds.clear();
22570
+ await Promise.all(itemIds.map((itemId) => {
22571
+ const text = this.planDeltaTextByItemId.get(itemId) ?? "";
22572
+ return text.length > 0 ? this.enqueuePlanSnapshot(itemId, text) : Promise.resolve();
22573
+ }));
22574
+ await this.planUpdateChain;
22575
+ } while (this.pendingPlanItemIds.size > 0);
22576
+ }
22577
+ async dispose() {
22578
+ if (this.disposed) return;
22579
+ await this.flushPendingPlanUpdates();
22580
+ this.disposed = true;
22581
+ this.cancelPlanUpdateTimer();
22582
+ this.pendingPlanItemIds.clear();
22583
+ this.planDeltaTextByItemId.clear();
22584
+ this.lastEmittedPlanTextByItemId.clear();
22585
+ }
22286
22586
  async createUpdateEvent(notification) {
22287
22587
  switch (notification.method) {
22288
22588
  case "item/agentMessage/delta":
22289
22589
  return await this.createTextEvent(notification.params);
22590
+ case "item/plan/delta":
22591
+ return this.createPlanDeltaEvent(notification.params);
22290
22592
  case "item/started":
22291
22593
  return await this.createItemEvent(notification.params);
22292
22594
  case "item/completed":
@@ -22299,6 +22601,8 @@ var CodexEventHandler = class {
22299
22601
  this.sessionState.currentTurnId = notification.params.turn.id;
22300
22602
  return null;
22301
22603
  case "turn/completed":
22604
+ await this.flushPendingPlanUpdates();
22605
+ this.clearPlanTurnState();
22302
22606
  this.sessionState.currentTurnId = null;
22303
22607
  return null;
22304
22608
  case "thread/tokenUsage/updated":
@@ -22397,7 +22701,6 @@ var CodexEventHandler = class {
22397
22701
  case "rawResponseItem/completed":
22398
22702
  case "rawResponse/completed":
22399
22703
  case "thread/started":
22400
- case "item/plan/delta":
22401
22704
  case "remoteControl/status/changed":
22402
22705
  case "app/list/updated":
22403
22706
  case "thread/settings/updated":
@@ -22416,10 +22719,14 @@ var CodexEventHandler = class {
22416
22719
  );
22417
22720
  return;
22418
22721
  case "item/plan/delta":
22419
- await this.emitCodexProposedPlanDelta(notification.params);
22722
+ if (!this.supportsPlanUpdates) {
22723
+ await this.emitCodexProposedPlanDelta(notification.params);
22724
+ }
22420
22725
  return;
22421
22726
  case "turn/completed":
22422
- await this.emitCodexProposedPlanCompleted(notification.params);
22727
+ if (!this.supportsPlanUpdates) {
22728
+ await this.emitCodexProposedPlanCompleted(notification.params);
22729
+ }
22423
22730
  return;
22424
22731
  default:
22425
22732
  return;
@@ -22518,14 +22825,20 @@ var CodexEventHandler = class {
22518
22825
  const detailsText = event.details ? `
22519
22826
 
22520
22827
  ${event.details}` : "";
22521
- return createAgentTextMessageChunk(`Config warning: ${event.summary}${detailsText}
22522
-
22523
- `);
22828
+ return this.createCodexSessionInfoUpdate({
22829
+ warning: {
22830
+ message: `${event.summary}${detailsText}`,
22831
+ source: "configWarning"
22832
+ }
22833
+ });
22524
22834
  }
22525
22835
  createWarningEvent(event) {
22526
- return createAgentTextMessageChunk(`Warning: ${event.message}
22527
-
22528
- `);
22836
+ return this.createCodexSessionInfoUpdate({
22837
+ warning: {
22838
+ message: event.message,
22839
+ source: "warning"
22840
+ }
22841
+ });
22529
22842
  }
22530
22843
  createModelReroutedEvent(event) {
22531
22844
  return createAgentTextThoughtChunk(`Model rerouted from ${event.fromModel} to ${event.toModel} (${event.reason}).
@@ -22533,8 +22846,9 @@ ${event.details}` : "";
22533
22846
  `);
22534
22847
  }
22535
22848
  createThreadGoalUpdatedEvent(event) {
22536
- const goalSnapshot = this.createThreadGoalSnapshot(event);
22537
- if (this.sameThreadGoalSnapshot(this.sessionState.currentGoal, goalSnapshot)) {
22849
+ this.sessionState.goalRevision += 1;
22850
+ const goalSnapshot = toThreadGoalSnapshot(event.goal);
22851
+ if (sameThreadGoalSnapshot(this.sessionState.currentGoal, goalSnapshot)) {
22538
22852
  return null;
22539
22853
  }
22540
22854
  this.sessionState.currentGoal = goalSnapshot;
@@ -22543,6 +22857,7 @@ ${event.details}` : "";
22543
22857
  });
22544
22858
  }
22545
22859
  createThreadGoalClearedEvent(_event) {
22860
+ this.sessionState.goalRevision += 1;
22546
22861
  if (this.sessionState.currentGoal === null) {
22547
22862
  return null;
22548
22863
  }
@@ -22551,16 +22866,6 @@ ${event.details}` : "";
22551
22866
  goal: null
22552
22867
  });
22553
22868
  }
22554
- createThreadGoalSnapshot(event) {
22555
- return {
22556
- objective: event.goal.objective.trim(),
22557
- status: event.goal.status,
22558
- tokenBudget: event.goal.tokenBudget
22559
- };
22560
- }
22561
- sameThreadGoalSnapshot(left, right) {
22562
- return left !== null && left !== void 0 && left.objective === right.objective && left.status === right.status && left.tokenBudget === right.tokenBudget;
22563
- }
22564
22869
  createReasoningSummaryDeltaEvent(event) {
22565
22870
  this.seenReasoningDeltaItemIds.add(event.itemId);
22566
22871
  let filter = this.reasoningSummaryFilters.get(event.itemId);
@@ -22575,6 +22880,19 @@ ${event.details}` : "";
22575
22880
  this.seenReasoningDeltaItemIds.add(event.itemId);
22576
22881
  return this.createAgentThoughtEvent(event.delta, event.itemId);
22577
22882
  }
22883
+ createPlanDeltaEvent(event) {
22884
+ if (event.delta.length === 0) {
22885
+ return null;
22886
+ }
22887
+ const text = this.planDeltaTextByItemId.get(event.itemId) ?? "";
22888
+ const updatedText = text + event.delta;
22889
+ this.planDeltaTextByItemId.set(event.itemId, updatedText);
22890
+ if (this.supportsPlanUpdates) {
22891
+ this.pendingPlanItemIds.add(event.itemId);
22892
+ this.schedulePlanUpdate();
22893
+ }
22894
+ return null;
22895
+ }
22578
22896
  createReasoningSectionBreakEvent(event) {
22579
22897
  this.seenReasoningDeltaItemIds.add(event.itemId);
22580
22898
  const trailingText = this.finishReasoningSummaryFilter(event.itemId);
@@ -22626,6 +22944,8 @@ ${event.details}` : "";
22626
22944
  case "contextCompaction":
22627
22945
  return createContextCompactionStartUpdate(event.item);
22628
22946
  case "subAgentActivity":
22947
+ this.activeSubAgentActivities.add(event.item.id);
22948
+ return createSubAgentActivityUpdate(event.item, "in_progress", "tool_call");
22629
22949
  case "sleep":
22630
22950
  case "userMessage":
22631
22951
  case "hookPrompt":
@@ -22678,17 +22998,23 @@ ${event.details}` : "";
22678
22998
  case "agentMessage":
22679
22999
  this.rememberAgentMessagePhase(event.item);
22680
23000
  return null;
23001
+ case "plan": {
23002
+ const deltaText = this.planDeltaTextByItemId.get(event.item.id) ?? "";
23003
+ return await this.createCompletedPlanEvent(event.item, deltaText);
23004
+ }
22681
23005
  case "exitedReviewMode":
22682
23006
  return this.createExitedReviewModeEvent(event.item);
22683
23007
  case "contextCompaction":
22684
23008
  return createContextCompactionCompleteUpdate(event.item);
22685
23009
  //ignored types
22686
- case "subAgentActivity":
23010
+ case "subAgentActivity": {
23011
+ const sessionUpdate = this.activeSubAgentActivities.delete(event.item.id) ? "tool_call_update" : "tool_call";
23012
+ return createSubAgentActivityUpdate(event.item, "completed", sessionUpdate);
23013
+ }
22687
23014
  case "sleep":
22688
23015
  case "userMessage":
22689
23016
  case "hookPrompt":
22690
23017
  case "enteredReviewMode":
22691
- case "plan":
22692
23018
  return null;
22693
23019
  }
22694
23020
  }
@@ -22702,6 +23028,71 @@ ${event.details}` : "";
22702
23028
  }
22703
23029
  return this.createAgentThoughtEvent(text, item.id);
22704
23030
  }
23031
+ async createCompletedPlanEvent(item, deltaText) {
23032
+ const text = item.text.length > 0 ? item.text : deltaText;
23033
+ this.pendingPlanItemIds.delete(item.id);
23034
+ if (this.pendingPlanItemIds.size === 0) {
23035
+ this.cancelPlanUpdateTimer();
23036
+ }
23037
+ this.planDeltaTextByItemId.delete(item.id);
23038
+ if (text.length === 0) {
23039
+ return null;
23040
+ }
23041
+ this.completedPlan = { itemId: item.id, text };
23042
+ if (this.supportsPlanUpdates) {
23043
+ await this.enqueuePlanSnapshot(item.id, text);
23044
+ return null;
23045
+ }
23046
+ return this.createPlanTextEvent(text, item.id);
23047
+ }
23048
+ schedulePlanUpdate() {
23049
+ if (this.disposed || this.planUpdateTimer !== null) return;
23050
+ this.planUpdateTimer = setTimeout(() => {
23051
+ this.planUpdateTimer = null;
23052
+ void this.flushPendingPlanUpdates().catch((error48) => {
23053
+ logger.error("Failed to flush throttled plan updates", error48);
23054
+ });
23055
+ }, _CodexEventHandler.PLAN_UPDATE_INTERVAL_MS);
23056
+ }
23057
+ cancelPlanUpdateTimer() {
23058
+ if (this.planUpdateTimer === null) return;
23059
+ clearTimeout(this.planUpdateTimer);
23060
+ this.planUpdateTimer = null;
23061
+ }
23062
+ enqueuePlanSnapshot(itemId, text) {
23063
+ const send = async () => {
23064
+ if (this.lastEmittedPlanTextByItemId.get(itemId) === text) return;
23065
+ await this.session.update(this.createPlanUpdateEvent(text, itemId));
23066
+ this.lastEmittedPlanTextByItemId.set(itemId, text);
23067
+ };
23068
+ const result = this.planUpdateChain.then(send);
23069
+ this.planUpdateChain = result.catch(() => {
23070
+ });
23071
+ return result;
23072
+ }
23073
+ clearPlanTurnState() {
23074
+ this.cancelPlanUpdateTimer();
23075
+ this.pendingPlanItemIds.clear();
23076
+ this.planDeltaTextByItemId.clear();
23077
+ this.lastEmittedPlanTextByItemId.clear();
23078
+ }
23079
+ createPlanUpdateEvent(text, planId) {
23080
+ return {
23081
+ sessionUpdate: "plan_update",
23082
+ plan: {
23083
+ type: "markdown",
23084
+ planId,
23085
+ content: text
23086
+ }
23087
+ };
23088
+ }
23089
+ createPlanTextEvent(text, messageId) {
23090
+ return createAgentTextMessageChunk(
23091
+ text,
23092
+ messageId,
23093
+ createCodexMessagePhaseMeta("final_answer")
23094
+ );
23095
+ }
22705
23096
  createExitedReviewModeEvent(item) {
22706
23097
  const text = item.review.trim();
22707
23098
  if (text.length === 0) {
@@ -22963,12 +23354,17 @@ var ApprovalOptionId = {
22963
23354
  AllowPermissionsForSession: "allow_permissions_session",
22964
23355
  RejectPermissions: "reject_permissions"
22965
23356
  };
22966
- function permissionOption(optionId, name, kind, codexMeta) {
23357
+ function permissionOption(optionId, name, kind, codexMeta, permission) {
22967
23358
  return {
22968
23359
  optionId,
22969
23360
  name,
22970
23361
  kind,
22971
- ...codexMeta ? { _meta: { codex: codexMeta } } : {}
23362
+ ...codexMeta || permission ? {
23363
+ _meta: {
23364
+ ...permission ? { permission } : {},
23365
+ ...codexMeta ? { codex: codexMeta } : {}
23366
+ }
23367
+ } : {}
22972
23368
  };
22973
23369
  }
22974
23370
  var CodexApprovalHandler = class {
@@ -23075,13 +23471,15 @@ var CodexApprovalHandler = class {
23075
23471
  ApprovalOptionId.AllowPermissionsForSession,
23076
23472
  "Allow for Session",
23077
23473
  "allow_always",
23078
- { decision: "allowPermissionsForSession", permissions: params.permissions }
23474
+ { decision: "allowPermissionsForSession", permissions: params.permissions },
23475
+ this.permissionGrantMetadata(params.permissions, "session")
23079
23476
  ),
23080
23477
  permissionOption(
23081
23478
  ApprovalOptionId.AllowPermissionsForTurn,
23082
23479
  "Allow Once",
23083
23480
  "allow_once",
23084
- { decision: "allowPermissionsForTurn", permissions: params.permissions }
23481
+ { decision: "allowPermissionsForTurn", permissions: params.permissions },
23482
+ this.permissionGrantMetadata(params.permissions, "turn")
23085
23483
  ),
23086
23484
  permissionOption(
23087
23485
  ApprovalOptionId.RejectPermissions,
@@ -23143,7 +23541,24 @@ var CodexApprovalHandler = class {
23143
23541
  ApprovalOptionId.AllowAlways,
23144
23542
  params.networkApprovalContext ? "Allow Host for Session" : "Allow for Session",
23145
23543
  "allow_always",
23146
- { decision: "acceptForSession" }
23544
+ { decision: "acceptForSession" },
23545
+ params.networkApprovalContext ? {
23546
+ version: 1,
23547
+ changes: [{
23548
+ type: "grant",
23549
+ operation: "grant",
23550
+ description: `Allow access to ${params.networkApprovalContext.host} for this session`,
23551
+ lifetime: { scope: "session" },
23552
+ targets: [{
23553
+ type: "network",
23554
+ matcher: {
23555
+ type: "host",
23556
+ host: params.networkApprovalContext.host,
23557
+ protocol: params.networkApprovalContext.protocol
23558
+ }
23559
+ }]
23560
+ }]
23561
+ } : void 0
23147
23562
  ),
23148
23563
  decision: "acceptForSession"
23149
23564
  }
@@ -23157,6 +23572,22 @@ var CodexApprovalHandler = class {
23157
23572
  {
23158
23573
  decision: "acceptWithExecpolicyAmendment",
23159
23574
  execpolicyAmendment: params.proposedExecpolicyAmendment
23575
+ },
23576
+ {
23577
+ version: 1,
23578
+ changes: [{
23579
+ type: "policy_rule",
23580
+ operation: "add",
23581
+ ruleBehavior: "allow",
23582
+ description: `Allow commands starting with ${params.proposedExecpolicyAmendment.join(" ")}`,
23583
+ targets: [{
23584
+ type: "command",
23585
+ matcher: {
23586
+ type: "argv_prefix",
23587
+ argv: params.proposedExecpolicyAmendment
23588
+ }
23589
+ }]
23590
+ }]
23160
23591
  }
23161
23592
  ),
23162
23593
  decision: {
@@ -23175,6 +23606,22 @@ var CodexApprovalHandler = class {
23175
23606
  {
23176
23607
  decision: "applyNetworkPolicyAmendment",
23177
23608
  networkPolicyAmendment: amendment
23609
+ },
23610
+ {
23611
+ version: 1,
23612
+ changes: [{
23613
+ type: "policy_rule",
23614
+ operation: "add",
23615
+ ruleBehavior: amendment.action,
23616
+ description: amendment.action === "allow" ? `Allow access to ${amendment.host}` : `Block access to ${amendment.host}`,
23617
+ targets: [{
23618
+ type: "network",
23619
+ matcher: {
23620
+ type: "host",
23621
+ host: amendment.host
23622
+ }
23623
+ }]
23624
+ }]
23178
23625
  }
23179
23626
  ),
23180
23627
  decision: {
@@ -23201,7 +23648,21 @@ var CodexApprovalHandler = class {
23201
23648
  ApprovalOptionId.AllowAlways,
23202
23649
  params.grantRoot ? "Allow Root for Session" : "Allow for Session",
23203
23650
  "allow_always",
23204
- { decision: "acceptForSession", grantRoot: params.grantRoot ?? null }
23651
+ { decision: "acceptForSession", grantRoot: params.grantRoot ?? null },
23652
+ params.grantRoot ? {
23653
+ version: 1,
23654
+ changes: [{
23655
+ type: "grant",
23656
+ operation: "grant",
23657
+ description: `Allow writes under ${params.grantRoot} for this session`,
23658
+ lifetime: { scope: "session" },
23659
+ targets: [{
23660
+ type: "filesystem",
23661
+ access: ["write"],
23662
+ matcher: { type: "directory", path: params.grantRoot }
23663
+ }]
23664
+ }]
23665
+ } : void 0
23205
23666
  ),
23206
23667
  decision: "acceptForSession"
23207
23668
  },
@@ -23224,6 +23685,68 @@ var CodexApprovalHandler = class {
23224
23685
  ...permissions.fileSystem ? { fileSystem: permissions.fileSystem } : {}
23225
23686
  };
23226
23687
  }
23688
+ permissionGrantMetadata(permissions, scope) {
23689
+ const changes = [];
23690
+ const lifetime = { scope };
23691
+ const suffix = scope === "session" ? " for this session" : " for this turn";
23692
+ if (permissions.network?.enabled !== null && permissions.network?.enabled !== void 0) {
23693
+ const allowed = permissions.network.enabled;
23694
+ changes.push({
23695
+ type: allowed ? "grant" : "policy_rule",
23696
+ operation: allowed ? "grant" : "add",
23697
+ ...allowed ? {} : { ruleBehavior: "deny" },
23698
+ description: `${allowed ? "Allow" : "Deny"} network access${suffix}`,
23699
+ lifetime,
23700
+ targets: [{ type: "network", matcher: { type: "any" } }]
23701
+ });
23702
+ }
23703
+ const fileSystem = permissions.fileSystem;
23704
+ for (const path6 of fileSystem?.read ?? []) {
23705
+ changes.push(this.fileSystemGrantChange(path6, "read", lifetime, suffix));
23706
+ }
23707
+ for (const path6 of fileSystem?.write ?? []) {
23708
+ changes.push(this.fileSystemGrantChange(path6, "write", lifetime, suffix));
23709
+ }
23710
+ for (const entry of fileSystem?.entries ?? []) {
23711
+ const matcher = (() => {
23712
+ switch (entry.path.type) {
23713
+ case "path":
23714
+ return { type: "exact_path", path: entry.path.path };
23715
+ case "glob_pattern":
23716
+ return { type: "glob", pattern: entry.path.pattern };
23717
+ case "special":
23718
+ return { type: "special", provider: "codex", value: entry.path.value };
23719
+ }
23720
+ })();
23721
+ const pathDescription = entry.path.type === "path" ? entry.path.path : entry.path.type === "glob_pattern" ? entry.path.pattern : JSON.stringify(entry.path.value);
23722
+ changes.push({
23723
+ type: entry.access === "deny" ? "policy_rule" : "grant",
23724
+ operation: entry.access === "deny" ? "add" : "grant",
23725
+ ...entry.access === "deny" ? { ruleBehavior: "deny" } : {},
23726
+ description: entry.access === "deny" ? `Deny filesystem access to ${pathDescription}${suffix}` : `Allow ${entry.access} access to ${pathDescription}${suffix}`,
23727
+ lifetime,
23728
+ targets: [{
23729
+ type: "filesystem",
23730
+ ...entry.access === "deny" ? {} : { access: [entry.access] },
23731
+ matcher
23732
+ }]
23733
+ });
23734
+ }
23735
+ return changes.length > 0 ? { version: 1, changes } : void 0;
23736
+ }
23737
+ fileSystemGrantChange(path6, access, lifetime, suffix) {
23738
+ return {
23739
+ type: "grant",
23740
+ operation: "grant",
23741
+ description: `Allow ${access} access to ${path6}${suffix}`,
23742
+ lifetime,
23743
+ targets: [{
23744
+ type: "filesystem",
23745
+ access: [access],
23746
+ matcher: { type: "exact_path", path: path6 }
23747
+ }]
23748
+ };
23749
+ }
23227
23750
  networkPolicyAmendmentOptionId(index) {
23228
23751
  return `${ApprovalOptionId.ApplyNetworkPolicyAmendment}:${index}`;
23229
23752
  }
@@ -24655,20 +25178,22 @@ var package_default = {
24655
25178
  "package:win-x64": "cd dist/bin && zip acp-extension-codex-x64-windows.zip acp-extension-codex-x64-windows.exe",
24656
25179
  "package:win-arm64": "cd dist/bin && zip acp-extension-codex-arm64-windows.zip acp-extension-codex-arm64-windows.exe",
24657
25180
  start: "node --import tsx src/index.ts",
25181
+ "example:steering": "node --import tsx examples/steering.ts",
25182
+ "example:steering:multistep": "node --import tsx examples/steering.ts",
24658
25183
  "generate-types": "./node_modules/.bin/codex app-server generate-ts --out src/app-server",
24659
25184
  test: "vitest run",
24660
25185
  "test:e2e": "npm run build && RUN_E2E_TESTS=true vitest run src/__tests__/CodexACPAgent/e2e",
24661
25186
  "test:watch": "vitest",
24662
- typecheck: "tsc --noEmit",
25187
+ typecheck: "tsc --noEmit && tsc --noEmit -p examples/tsconfig.json",
24663
25188
  "codex-test": "tsx .claude/skills/run-codex/scripts/run-codex-test.ts"
24664
25189
  },
24665
- homepage: "https://github.com/Leeeon233/acp-extension-codex#readme",
25190
+ homepage: "https://github.com/loro-dev/acp-extension-codex#readme",
24666
25191
  bugs: {
24667
- url: "https://github.com/Leeeon233/acp-extension-codex/issues"
25192
+ url: "https://github.com/loro-dev/acp-extension-codex/issues"
24668
25193
  },
24669
25194
  repository: {
24670
25195
  type: "git",
24671
- url: "git+https://github.com/Leeeon233/acp-extension-codex.git"
25196
+ url: "git+https://github.com/loro-dev/acp-extension-codex.git"
24672
25197
  },
24673
25198
  keywords: [
24674
25199
  "codex",
@@ -24685,12 +25210,12 @@ var package_default = {
24685
25210
  "@types/node": "^26.1.0",
24686
25211
  esbuild: "^0.28.1",
24687
25212
  "mcp-hello-world": "^1.1.2",
24688
- tsx: "^4.23.0",
24689
- typescript: "^6.0.3",
25213
+ tsx: "^4.23.1",
25214
+ typescript: "^7.0.2",
24690
25215
  vitest: "^4.1.10"
24691
25216
  },
24692
25217
  dependencies: {
24693
- "@agentclientprotocol/sdk": "^1.2.1",
25218
+ "@agentclientprotocol/sdk": "^1.3.0",
24694
25219
  "@openai/codex": "^0.145.0",
24695
25220
  diff: "^9.0.0",
24696
25221
  open: "^11.0.0",
@@ -24698,6 +25223,39 @@ var package_default = {
24698
25223
  zod: "^4.0.0"
24699
25224
  }
24700
25225
  };
25226
+ var COLLABORATION_MODE_CONFIG_ID = "collaboration_mode";
25227
+ var DEFAULT_COLLABORATION_MODE = "default";
25228
+ var PLAN_COLLABORATION_MODE = "plan";
25229
+ function createCollaborationModeConfigOption(currentValue) {
25230
+ return {
25231
+ id: COLLABORATION_MODE_CONFIG_ID,
25232
+ name: "Collaboration mode",
25233
+ description: "How Codex collaborates for subsequent turns",
25234
+ category: "collaboration_mode",
25235
+ type: "select",
25236
+ currentValue,
25237
+ options: [
25238
+ { value: DEFAULT_COLLABORATION_MODE, name: "Default" },
25239
+ { value: PLAN_COLLABORATION_MODE, name: "Plan", description: "Plan before making changes" }
25240
+ ]
25241
+ };
25242
+ }
25243
+ function parseCollaborationMode(value) {
25244
+ if (value === DEFAULT_COLLABORATION_MODE) return DEFAULT_COLLABORATION_MODE;
25245
+ if (value === PLAN_COLLABORATION_MODE) return PLAN_COLLABORATION_MODE;
25246
+ return null;
25247
+ }
25248
+ function createCodexCollaborationMode(mode, currentModelId) {
25249
+ const modelId = ModelId.fromString(currentModelId);
25250
+ return {
25251
+ mode,
25252
+ settings: {
25253
+ model: modelId.model,
25254
+ reasoning_effort: modelId.effort,
25255
+ developer_instructions: null
25256
+ }
25257
+ };
25258
+ }
24701
25259
  var CUSTOM_GATEWAY_PROVIDER_ID = "custom-gateway";
24702
25260
  var SESSION_LIST_PAGE_SIZE = 100;
24703
25261
  var SUPPORTED_GATEWAY_PROTOCOLS = {
@@ -24712,6 +25270,7 @@ var CodexAcpClient = class {
24712
25270
  pendingAccountUpdated = null;
24713
25271
  sessionNotificationQueues = /* @__PURE__ */ new Map();
24714
25272
  skillExtraRoots = [];
25273
+ configPath = null;
24715
25274
  constructor(codexClient, codexConfig, modelProvider) {
24716
25275
  this.codexClient = codexClient;
24717
25276
  this.config = codexConfig ?? {};
@@ -24724,7 +25283,7 @@ var CodexAcpClient = class {
24724
25283
  version: `${package_default.version}`
24725
25284
  };
24726
25285
  async initialize(request) {
24727
- await this.codexClient.initialize({
25286
+ const response = await this.codexClient.initialize({
24728
25287
  capabilities: {
24729
25288
  experimentalApi: true,
24730
25289
  requestAttestation: false
@@ -24735,6 +25294,10 @@ var CodexAcpClient = class {
24735
25294
  title: request.clientInfo?.title ?? this.defaultClientInfo.title
24736
25295
  }
24737
25296
  });
25297
+ this.configPath = response?.codexHome ?? null;
25298
+ }
25299
+ getHomePath() {
25300
+ return this.configPath;
24738
25301
  }
24739
25302
  async authenticate(authRequest) {
24740
25303
  if (!isCodexAuthRequest(authRequest)) {
@@ -24944,6 +25507,7 @@ var CodexAcpClient = class {
24944
25507
  sessionId: request.sessionId,
24945
25508
  currentModelId,
24946
25509
  models: codexModels,
25510
+ collaborationMode: this.getCollaborationMode(response.thread.id),
24947
25511
  modelProvider: response.modelProvider,
24948
25512
  currentServiceTier: response.serviceTier ?? null,
24949
25513
  additionalDirectories
@@ -24968,6 +25532,7 @@ var CodexAcpClient = class {
24968
25532
  sessionId: response.thread.id,
24969
25533
  currentModelId,
24970
25534
  models: codexModels,
25535
+ collaborationMode: this.getCollaborationMode(response.thread.id),
24971
25536
  modelProvider: response.modelProvider,
24972
25537
  currentServiceTier: response.serviceTier ?? null,
24973
25538
  additionalDirectories
@@ -24993,6 +25558,7 @@ var CodexAcpClient = class {
24993
25558
  sessionId: request.sessionId,
24994
25559
  currentModelId,
24995
25560
  models: codexModels,
25561
+ collaborationMode: this.getCollaborationMode(response.thread.id),
24996
25562
  modelProvider: response.modelProvider,
24997
25563
  currentServiceTier: response.serviceTier ?? null,
24998
25564
  thread: historyResponse.thread,
@@ -25017,6 +25583,7 @@ var CodexAcpClient = class {
25017
25583
  sessionId: response.thread.id,
25018
25584
  currentModelId,
25019
25585
  models: codexModels,
25586
+ collaborationMode: this.getCollaborationMode(response.thread.id),
25020
25587
  modelProvider: response.modelProvider,
25021
25588
  currentServiceTier: response.serviceTier ?? null,
25022
25589
  additionalDirectories
@@ -25042,6 +25609,10 @@ var CodexAcpClient = class {
25042
25609
  async runCompact(sessionId) {
25043
25610
  await this.codexClient.runCompact({ threadId: sessionId });
25044
25611
  }
25612
+ async getGoal(sessionId) {
25613
+ const response = await this.codexClient.threadGoalGet({ threadId: sessionId });
25614
+ return response?.goal ?? null;
25615
+ }
25045
25616
  async setGoal(sessionId, objective, onTurnStarted) {
25046
25617
  return await this.codexClient.runGoalSet({
25047
25618
  threadId: sessionId,
@@ -25050,10 +25621,17 @@ var CodexAcpClient = class {
25050
25621
  }, onTurnStarted);
25051
25622
  }
25052
25623
  async setGoalStatus(sessionId, status) {
25624
+ let updatedGoal = null;
25053
25625
  await this.codexClient.runGoalSet({
25054
25626
  threadId: sessionId,
25055
25627
  status
25628
+ }, void 0, void 0, (goal) => {
25629
+ updatedGoal = goal;
25056
25630
  });
25631
+ if (updatedGoal === null) {
25632
+ throw new Error(`Goal update for session ${sessionId} returned no goal`);
25633
+ }
25634
+ return updatedGoal;
25057
25635
  }
25058
25636
  async resumeGoal(sessionId, onTurnStarted) {
25059
25637
  return await this.codexClient.runGoalSet({
@@ -25101,11 +25679,16 @@ var CodexAcpClient = class {
25101
25679
  }
25102
25680
  async getConfigMcpServerNames(projectPath) {
25103
25681
  const response = await this.codexClient.configRead({ includeLayers: true, cwd: projectPath });
25104
- const mcpServers = response?.config?.["mcp_servers"];
25105
- if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
25682
+ const effectiveMcpServers = response?.config?.["mcp_servers"];
25683
+ const configLayers = response?.layers ?? [];
25684
+ const layerMcpServers = configLayers.map((layer) => {
25685
+ return isJsonObject(layer.config) ? layer.config["mcp_servers"] : void 0;
25686
+ });
25687
+ const configuredMcpServers = [effectiveMcpServers, ...layerMcpServers].filter(isJsonObject);
25688
+ if (configuredMcpServers.length === 0) {
25106
25689
  return /* @__PURE__ */ new Set();
25107
25690
  }
25108
- return new Set(Object.keys(mcpServers));
25691
+ return new Set(configuredMcpServers.flatMap((server) => Object.keys(server)));
25109
25692
  }
25110
25693
  getModelProvider() {
25111
25694
  return this.gatewayConfig?.modelProvider ?? this.modelProvider;
@@ -25221,7 +25804,7 @@ var CodexAcpClient = class {
25221
25804
  }
25222
25805
  });
25223
25806
  }
25224
- async sendPrompt(request, agentMode, modelId, serviceTier, collaborationMode, disableSummary, cwd, additionalDirectories, onTurnStarted, shouldCancel) {
25807
+ async sendPrompt(request, agentMode, modelId, serviceTier, disableSummary, cwd, additionalDirectories, onTurnStarted, shouldCancel) {
25225
25808
  const input = buildPromptItems(request.prompt);
25226
25809
  const effort = modelId.effort;
25227
25810
  await this.refreshSkills(cwd, additionalDirectories);
@@ -25239,19 +25822,16 @@ var CodexAcpClient = class {
25239
25822
  model: modelId.model,
25240
25823
  serviceTier
25241
25824
  };
25242
- if (collaborationMode !== null) {
25243
- params.collaborationMode = collaborationMode;
25244
- }
25245
25825
  return await this.codexClient.runTurn(params, onTurnStarted);
25246
25826
  }
25247
- async sendSteer(request, expectedTurnId, steerId) {
25248
- const response = await this.codexClient.turnSteer({
25249
- threadId: request.sessionId,
25250
- input: buildPromptItems(request.prompt),
25251
- expectedTurnId,
25252
- clientUserMessageId: steerId
25827
+ async setCollaborationMode(sessionId, mode, currentModelId) {
25828
+ await this.codexClient.threadSettingsUpdate({
25829
+ threadId: sessionId,
25830
+ collaborationMode: createCodexCollaborationMode(mode, currentModelId)
25253
25831
  });
25254
- return response.turnId;
25832
+ }
25833
+ getCollaborationMode(sessionId) {
25834
+ return this.codexClient.getThreadSettings(sessionId)?.collaborationMode.mode ?? "default";
25255
25835
  }
25256
25836
  resolveTurnInterrupted(params) {
25257
25837
  this.codexClient.resolveTurnInterrupted(params.threadId, params.turnId);
@@ -25360,6 +25940,14 @@ var CodexAcpClient = class {
25360
25940
  turnId: params.turnId
25361
25941
  });
25362
25942
  }
25943
+ async steerTurn(params) {
25944
+ return await this.codexClient.turnSteer({
25945
+ threadId: params.threadId,
25946
+ expectedTurnId: params.turnId,
25947
+ input: buildPromptItems(params.prompt),
25948
+ ...params.steerId ? { clientUserMessageId: params.steerId } : {}
25949
+ });
25950
+ }
25363
25951
  async fetchAvailableModels() {
25364
25952
  const models = [];
25365
25953
  let cursor = null;
@@ -25670,6 +26258,20 @@ var CodexCommands = class {
25670
26258
  */
25671
26259
  getBuiltinCommands() {
25672
26260
  return [
26261
+ {
26262
+ name: "plan",
26263
+ description: "Turn plan mode on.",
26264
+ input: null,
26265
+ _meta: {
26266
+ commandAction: {
26267
+ kind: "setConfigOption",
26268
+ configId: COLLABORATION_MODE_CONFIG_ID,
26269
+ value: PLAN_COLLABORATION_MODE,
26270
+ resetValue: DEFAULT_COLLABORATION_MODE,
26271
+ presentation: "state"
26272
+ }
26273
+ }
26274
+ },
25673
26275
  {
25674
26276
  name: "mcp",
25675
26277
  description: "List configured Model Context Protocol (MCP) tools.",
@@ -25707,8 +26309,14 @@ var CodexCommands = class {
25707
26309
  },
25708
26310
  {
25709
26311
  name: "goal",
25710
- description: "Set, pause, resume, or clear a task goal.",
25711
- input: { hint: "[<objective>|clear|pause|resume]" }
26312
+ description: "Set a goal to keep pursuing.",
26313
+ input: { hint: "[<objective>|clear|pause|resume]" },
26314
+ _meta: {
26315
+ commandAction: {
26316
+ kind: "prefixPrompt",
26317
+ presentation: "state"
26318
+ }
26319
+ }
25712
26320
  },
25713
26321
  {
25714
26322
  name: "logout",
@@ -25738,6 +26346,15 @@ var CodexCommands = class {
25738
26346
  if (commandName.startsWith("$")) return { handled: false };
25739
26347
  const sessionId = sessionState.sessionId;
25740
26348
  switch (commandName) {
26349
+ case "plan": {
26350
+ if (command.rest.length > 0) {
26351
+ await this.sendCommandUsageMessage(commandName, "no arguments", sessionId);
26352
+ return { handled: true };
26353
+ }
26354
+ const mode = sessionState.collaborationMode === PLAN_COLLABORATION_MODE ? DEFAULT_COLLABORATION_MODE : PLAN_COLLABORATION_MODE;
26355
+ await options.setConfigOption?.(COLLABORATION_MODE_CONFIG_ID, mode);
26356
+ return { handled: options.setConfigOption !== void 0 };
26357
+ }
25741
26358
  case "compact": {
25742
26359
  await this.runWithProcessCheck(() => this.codexAcpClient.runCompact(sessionId));
25743
26360
  return { handled: true };
@@ -26023,6 +26640,45 @@ var CodexCommands = class {
26023
26640
  return count.toString();
26024
26641
  }
26025
26642
  };
26643
+ var SteeringQueue = class {
26644
+ constructor(handle) {
26645
+ this.handle = handle;
26646
+ }
26647
+ handle;
26648
+ pending = [];
26649
+ processing = false;
26650
+ enqueue(params) {
26651
+ return new Promise((resolve, reject) => {
26652
+ this.pending.push({ params, resolve, reject });
26653
+ this.startConsumer();
26654
+ });
26655
+ }
26656
+ /** No request is queued and the consumer is not running. */
26657
+ get isIdle() {
26658
+ return !this.processing && this.pending.length === 0;
26659
+ }
26660
+ startConsumer() {
26661
+ if (this.processing) {
26662
+ return;
26663
+ }
26664
+ this.processing = true;
26665
+ void this.consume();
26666
+ }
26667
+ async consume() {
26668
+ try {
26669
+ while (this.pending.length > 0) {
26670
+ const next = this.pending.shift();
26671
+ try {
26672
+ next.resolve(await this.handle(next.params));
26673
+ } catch (error48) {
26674
+ next.reject(error48);
26675
+ }
26676
+ }
26677
+ } finally {
26678
+ this.processing = false;
26679
+ }
26680
+ }
26681
+ };
26026
26682
  function historyFallbackUpdateKey(update) {
26027
26683
  switch (update.sessionUpdate) {
26028
26684
  case "user_message_chunk":
@@ -27025,32 +27681,6 @@ function createFastModeConfigOption(fastModeEnabled, useBooleanConfigOption = fa
27025
27681
  ]
27026
27682
  };
27027
27683
  }
27028
- var PLAN_MODE_CONFIG_ID = "plan-mode";
27029
- var PLAN_MODE_ON = "on";
27030
- var PLAN_MODE_OFF = "off";
27031
- var PLAN_MODE_DESCRIPTION = "Plan without modifying files; switch off to implement the approved plan";
27032
- function createPlanModeConfigOption(planModeEnabled) {
27033
- return {
27034
- id: PLAN_MODE_CONFIG_ID,
27035
- name: "Plan mode",
27036
- description: PLAN_MODE_DESCRIPTION,
27037
- category: PLAN_MODE_CONFIG_ID,
27038
- type: "select",
27039
- currentValue: planModeEnabled ? PLAN_MODE_ON : PLAN_MODE_OFF,
27040
- options: [
27041
- {
27042
- value: PLAN_MODE_OFF,
27043
- name: "Off",
27044
- description: "Implement changes normally"
27045
- },
27046
- {
27047
- value: PLAN_MODE_ON,
27048
- name: "On",
27049
- description: PLAN_MODE_DESCRIPTION
27050
- }
27051
- ]
27052
- };
27053
- }
27054
27684
  function isJetBrains2026_1Client(clientInfo) {
27055
27685
  if (!clientInfo) {
27056
27686
  return false;
@@ -27060,6 +27690,11 @@ function isJetBrains2026_1Client(clientInfo) {
27060
27690
  const isJetBrainsClient = clientInfo.name.startsWith("JetBrains");
27061
27691
  return (isIntelliJPlatform || isJetBrainsClient) && clientInfo.version.startsWith("2026.1");
27062
27692
  }
27693
+ function clientSupportsPlanUpdates(clientCapabilities) {
27694
+ return clientCapabilities?.plan != null;
27695
+ }
27696
+ var IMPLEMENT_PLAN_OPTION_ID = "implement_plan";
27697
+ var REVISE_PLAN_OPTION_ID = "revise_plan";
27063
27698
  var CodexAcpServer = class _CodexAcpServer {
27064
27699
  static MODEL_NAME_TOKEN_OVERRIDES = {
27065
27700
  gpt: "GPT",
@@ -27081,6 +27716,7 @@ var CodexAcpServer = class _CodexAcpServer {
27081
27716
  pendingTurnStarts;
27082
27717
  activePrompts;
27083
27718
  pendingSteers;
27719
+ steeringQueues;
27084
27720
  closingSessions;
27085
27721
  sessionGenerations;
27086
27722
  sessionOpenGenerations;
@@ -27090,6 +27726,7 @@ var CodexAcpServer = class _CodexAcpServer {
27090
27726
  this.pendingTurnStarts = /* @__PURE__ */ new Map();
27091
27727
  this.activePrompts = /* @__PURE__ */ new Map();
27092
27728
  this.pendingSteers = /* @__PURE__ */ new Map();
27729
+ this.steeringQueues = /* @__PURE__ */ new Map();
27093
27730
  this.closingSessions = /* @__PURE__ */ new Map();
27094
27731
  this.sessionGenerations = /* @__PURE__ */ new Map();
27095
27732
  this.sessionOpenGenerations = /* @__PURE__ */ new Map();
@@ -27155,7 +27792,12 @@ var CodexAcpServer = class _CodexAcpServer {
27155
27792
  }
27156
27793
  }
27157
27794
  },
27158
- authMethods: getCodexAuthMethods(_params.clientCapabilities)
27795
+ authMethods: getCodexAuthMethods(_params.clientCapabilities),
27796
+ _meta: {
27797
+ steering: {
27798
+ supported: true
27799
+ }
27800
+ }
27159
27801
  };
27160
27802
  }
27161
27803
  async extMethod(method, params) {
@@ -27172,6 +27814,27 @@ var CodexAcpServer = class _CodexAcpServer {
27172
27814
  }
27173
27815
  case LEGACY_SET_SESSION_MODEL_METHOD:
27174
27816
  return await this.unstable_setSessionModel(this.parseLegacySetSessionModelParams(methodRequest.params));
27817
+ case SESSION_STEERING_METHOD:
27818
+ return await this.executeOrQueueSteeringRequest(this.parseSessionSteerParams(methodRequest.params));
27819
+ case GOAL_CONTROL_METHOD: {
27820
+ const sessionState = this.sessions.get(methodRequest.params.sessionId);
27821
+ if (!sessionState) {
27822
+ throw RequestError.invalidParams(void 0, `Unknown session: ${methodRequest.params.sessionId}`);
27823
+ }
27824
+ const sessionGeneration = this.getSessionGeneration(sessionState.sessionId);
27825
+ if (methodRequest.params.action === "pause") {
27826
+ const goal = await this.runWithProcessCheck(() => this.codexAcpClient.setGoalStatus(sessionState.sessionId, "paused"));
27827
+ if (this.goalPublishIsCurrent(sessionState, sessionGeneration)) {
27828
+ await this.publishGoalSnapshot(sessionState, toThreadGoalSnapshot(goal), false);
27829
+ }
27830
+ } else if (methodRequest.params.action === "clear") {
27831
+ await this.runWithProcessCheck(() => this.codexAcpClient.clearGoal(sessionState.sessionId));
27832
+ if (this.goalPublishIsCurrent(sessionState, sessionGeneration)) {
27833
+ await this.publishGoalSnapshot(sessionState, null, false);
27834
+ }
27835
+ }
27836
+ return {};
27837
+ }
27175
27838
  }
27176
27839
  }
27177
27840
  async checkAuthorization() {
@@ -27216,6 +27879,12 @@ var CodexAcpServer = class _CodexAcpServer {
27216
27879
 
27217
27880
  You have been logged out. Please try again.`);
27218
27881
  }
27882
+ const configPath = this.codexAcpClient.getHomePath() ?? "global";
27883
+ if (e.message.includes("load config")) {
27884
+ throw RequestError.internalError(`${e.message}
27885
+
27886
+ Check ${configPath} and project .codex directories, especially their config.toml files, or any CODEX_CONFIG override.`);
27887
+ }
27219
27888
  }
27220
27889
  beginSessionOpen(sessionId) {
27221
27890
  const generation = this.getSessionGeneration(sessionId);
@@ -27368,6 +28037,7 @@ You have been logged out. Please try again.`);
27368
28037
  supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [],
27369
28038
  supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"],
27370
28039
  agentMode: AgentMode.getInitialAgentMode(),
28040
+ collaborationMode: sessionMetadata.collaborationMode,
27371
28041
  currentTurnId: null,
27372
28042
  lastTokenUsage: null,
27373
28043
  totalTokenUsage: null,
@@ -27380,10 +28050,9 @@ You have been logged out. Please try again.`);
27380
28050
  additionalDirectories: sessionMetadata.additionalDirectories,
27381
28051
  fastModeEnabled: sessionMetadata.currentServiceTier === "fast",
27382
28052
  currentModelSupportsFast,
27383
- planModeEnabled: false,
27384
- planModeExplicitlySet: false,
27385
28053
  sessionMcpServers,
27386
28054
  terminalOutputMode: this.terminalOutputMode,
28055
+ goalRevision: 0,
27387
28056
  sessionTitle: null,
27388
28057
  sessionTitleSource: operation.kind === "new" ? "unset" : "unknown"
27389
28058
  };
@@ -27398,6 +28067,9 @@ You have been logged out. Please try again.`);
27398
28067
  this.publishMcpStartupStatusAsync(sessionId);
27399
28068
  }
27400
28069
  const availableCommands = await this.availableCommands.getAvailableCommands(sessionState);
28070
+ if ("sessionId" in request) {
28071
+ this.publishCurrentGoalAsync(sessionState, openedSession.generation);
28072
+ }
27401
28073
  const sessionModelState = this.createModelState(models, currentModelId);
27402
28074
  const sessionModeState = sessionState.agentMode.toSessionModeState();
27403
28075
  return [sessionId, sessionModelState, sessionModeState, availableCommands];
@@ -27546,6 +28218,8 @@ You have been logged out. Please try again.`);
27546
28218
  this.pendingMcpStartupSessions.delete(params.sessionId);
27547
28219
  this.pendingTurnStarts.delete(params.sessionId);
27548
28220
  this.activePrompts.delete(params.sessionId);
28221
+ this.pendingSteers.delete(params.sessionId);
28222
+ this.steeringQueues.delete(params.sessionId);
27549
28223
  }
27550
28224
  this.endSessionCloseFence(params.sessionId);
27551
28225
  }
@@ -27657,16 +28331,22 @@ You have been logged out. Please try again.`);
27657
28331
  });
27658
28332
  const sessionState = this.sessions.get(params.sessionId);
27659
28333
  if (!sessionState) throw new Error(`Session ${params.sessionId} not found`);
28334
+ await this.applySessionConfigOption(sessionState, params);
28335
+ return {
28336
+ configOptions: this.createSessionConfigOptions(sessionState)
28337
+ };
28338
+ }
28339
+ async applySessionConfigOption(sessionState, params) {
27660
28340
  switch (params.configId) {
27661
28341
  case FAST_MODE_CONFIG_ID:
27662
28342
  this.applyFastModeChange(sessionState, params);
27663
28343
  break;
27664
- case PLAN_MODE_CONFIG_ID:
27665
- this.applyPlanModeChange(sessionState, this.stringConfigValue(params));
27666
- break;
27667
28344
  case MODE_CONFIG_ID:
27668
28345
  this.applyModeChange(sessionState, this.stringConfigValue(params));
27669
28346
  break;
28347
+ case COLLABORATION_MODE_CONFIG_ID:
28348
+ await this.applyCollaborationModeChange(sessionState, this.stringConfigValue(params));
28349
+ break;
27670
28350
  case MODEL_CONFIG_ID:
27671
28351
  this.applyModelChange(sessionState, this.stringConfigValue(params));
27672
28352
  break;
@@ -27676,9 +28356,6 @@ You have been logged out. Please try again.`);
27676
28356
  default:
27677
28357
  throw RequestError.invalidParams();
27678
28358
  }
27679
- return {
27680
- configOptions: this.createSessionConfigOptions(sessionState)
27681
- };
27682
28359
  }
27683
28360
  applyFastModeChange(sessionState, params) {
27684
28361
  const value = params.value;
@@ -27691,13 +28368,6 @@ You have been logged out. Please try again.`);
27691
28368
  }
27692
28369
  sessionState.fastModeEnabled = value === FAST_MODE_ON;
27693
28370
  }
27694
- applyPlanModeChange(sessionState, value) {
27695
- if (value !== PLAN_MODE_ON && value !== PLAN_MODE_OFF) {
27696
- throw RequestError.invalidParams();
27697
- }
27698
- sessionState.planModeEnabled = value === PLAN_MODE_ON;
27699
- sessionState.planModeExplicitlySet = true;
27700
- }
27701
28371
  stringConfigValue(params) {
27702
28372
  if (typeof params.value !== "string") {
27703
28373
  throw RequestError.invalidParams();
@@ -27711,6 +28381,14 @@ You have been logged out. Please try again.`);
27711
28381
  }
27712
28382
  sessionState.agentMode = newMode;
27713
28383
  }
28384
+ async applyCollaborationModeChange(sessionState, value) {
28385
+ const mode = parseCollaborationMode(value);
28386
+ if (mode === null) {
28387
+ throw RequestError.invalidParams();
28388
+ }
28389
+ await this.codexAcpClient.setCollaborationMode(sessionState.sessionId, mode, sessionState.currentModelId);
28390
+ sessionState.collaborationMode = mode;
28391
+ }
27714
28392
  applyModelChange(sessionState, value) {
27715
28393
  const model = sessionState.availableModels.find((m) => m.id === value);
27716
28394
  if (!model) {
@@ -27774,10 +28452,239 @@ You have been logged out. Please try again.`);
27774
28452
  modelId
27775
28453
  };
27776
28454
  }
28455
+ /**
28456
+ * Handles one incoming steering request, serialising it against any other
28457
+ * steer already in flight for the same session.
28458
+ *
28459
+ * Every session gets its own {@link SteeringQueue}: the request is enqueued
28460
+ * and awaited, so concurrent steers for one session run strictly one at a
28461
+ * time, in arrival order, and can never race to inject into — or start —
28462
+ * rival turns. Steers for different sessions use different queues and run
28463
+ * concurrently. Once the queue drains to idle it is removed from the map,
28464
+ * so no per-session entry leaks after the session goes quiet (the identity
28465
+ * check guards against deleting a queue a later request has since reused).
28466
+ *
28467
+ * @param params The target session id and the prompt to steer with.
28468
+ * @returns Whether the prompt joined the active turn ("injected"), started a
28469
+ * new one ("startedNewTurn"), or could not be applied ("failed"); see
28470
+ * {@link performSteeringRequest}.
28471
+ */
28472
+ async executeOrQueueSteeringRequest(params) {
28473
+ const queue = this.getSteeringQueue(params.sessionId);
28474
+ try {
28475
+ return await queue.enqueue(params);
28476
+ } catch (error48) {
28477
+ if (error48 instanceof RequestError) {
28478
+ throw error48;
28479
+ }
28480
+ logger.error(`Steering request for session ${params.sessionId} failed`, error48);
28481
+ return { outcome: "failed" };
28482
+ } finally {
28483
+ if (queue.isIdle && this.steeringQueues.get(params.sessionId) === queue) {
28484
+ this.steeringQueues.delete(params.sessionId);
28485
+ }
28486
+ }
28487
+ }
28488
+ /**
28489
+ * Returns the steering queue for a session, creating and registering it on
28490
+ * first use.
28491
+ *
28492
+ * @param sessionId The session whose steering queue is required.
28493
+ * @returns The session's existing queue, or a freshly created one.
28494
+ */
28495
+ getSteeringQueue(sessionId) {
28496
+ let queue = this.steeringQueues.get(sessionId);
28497
+ if (!queue) {
28498
+ queue = new SteeringQueue((params) => this.performSteeringRequest(params));
28499
+ this.steeringQueues.set(sessionId, queue);
28500
+ }
28501
+ return queue;
28502
+ }
28503
+ /**
28504
+ * Delivers a steering prompt to the session: injects it into the live turn
28505
+ * when there is one, otherwise starts a new turn.
28506
+ *
28507
+ * @param params The target session id and the prompt to steer with.
28508
+ * @returns "injected" when the prompt joined an existing turn, otherwise the
28509
+ * outcome of starting a new turn.
28510
+ */
28511
+ async performSteeringRequest(params) {
28512
+ logger.log("Steering session requested", {
28513
+ sessionId: params.sessionId,
28514
+ prompt: params.prompt
28515
+ });
28516
+ const sessionState = this.getSessionState(params.sessionId);
28517
+ this.assertSteerInputSupported(params, sessionState);
28518
+ const turnId = await this.getSteerableTurnId(sessionState);
28519
+ if (turnId) {
28520
+ const injected = await this.injectSteerIntoActiveTurn(params, turnId, sessionState);
28521
+ if (injected) {
28522
+ logger.log("Steering session injected", { sessionId: params.sessionId, turnId });
28523
+ return { outcome: "injected" };
28524
+ }
28525
+ }
28526
+ if (params.steerId) {
28527
+ throw RequestError.invalidRequest("No active Codex turn to steer");
28528
+ }
28529
+ return await this.startNewTurnFromSteering(params);
28530
+ }
28531
+ /**
28532
+ * Rejects a steering prompt whose content the active model cannot accept
28533
+ * (currently: image blocks on a text-only model).
28534
+ */
28535
+ assertSteerInputSupported(params, sessionState) {
28536
+ const hasImage = params.prompt.some((block) => block.type === "image");
28537
+ if (hasImage && !sessionState.supportedInputModalities.includes("image")) {
28538
+ throw RequestError.invalidRequest("The current model does not support image input");
28539
+ }
28540
+ }
28541
+ /**
28542
+ * Attempts to inject the prompt into the given running turn.
28543
+ *
28544
+ * A failed injection is fatal only when the turn is still the session's
28545
+ * current turn and Codex reported something other than "no active turn to
28546
+ * steer". Otherwise the turn has already ended underneath us and the caller
28547
+ * should start a new turn instead.
28548
+ *
28549
+ * @returns true when the prompt was injected; false when the caller should
28550
+ * fall back to starting a new turn.
28551
+ */
28552
+ async injectSteerIntoActiveTurn(params, turnId, sessionState) {
28553
+ const activePrompt = this.activePrompts.get(params.sessionId);
28554
+ const activeTurn = activePrompt?.currentTurn;
28555
+ if (params.steerId) {
28556
+ const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
28557
+ if (firstText.startsWith("/")) {
28558
+ throw RequestError.invalidRequest("Slash commands cannot steer an active Codex turn");
28559
+ }
28560
+ if (!activePrompt || !activeTurn || activeTurn.turnId !== turnId || activePrompt.signal.aborted) {
28561
+ return false;
28562
+ }
28563
+ }
28564
+ const pending = params.steerId ? this.pendingSteers.get(params.sessionId) ?? /* @__PURE__ */ new Map() : null;
28565
+ if (params.steerId && activePrompt && pending) {
28566
+ if (pending.has(params.steerId)) {
28567
+ throw RequestError.invalidRequest(`Duplicate Codex steer id: ${params.steerId}`);
28568
+ }
28569
+ pending.set(params.steerId, { activePrompt, turnId });
28570
+ this.pendingSteers.set(params.sessionId, pending);
28571
+ }
28572
+ try {
28573
+ const response = await this.runWithProcessCheck(() => this.codexAcpClient.steerTurn({
28574
+ threadId: params.steerId && activeTurn ? activeTurn.threadId : params.sessionId,
28575
+ turnId,
28576
+ prompt: params.prompt,
28577
+ ...params.steerId ? { steerId: params.steerId } : {}
28578
+ }));
28579
+ if (response.turnId !== turnId) {
28580
+ throw RequestError.internalError(
28581
+ { expectedTurnId: turnId, actualTurnId: response.turnId },
28582
+ `Codex steered unexpected turn ${response.turnId}; expected ${turnId}`
28583
+ );
28584
+ }
28585
+ return true;
28586
+ } catch (err) {
28587
+ if (params.steerId && activePrompt && pending?.get(params.steerId)?.activePrompt === activePrompt) {
28588
+ pending.delete(params.steerId);
28589
+ if (pending.size === 0) this.pendingSteers.delete(params.sessionId);
28590
+ }
28591
+ await this.codexAcpClient.waitForSessionNotifications(params.sessionId);
28592
+ const turnStillActive = sessionState.currentTurnId === turnId;
28593
+ if (turnStillActive && !this.isNoActiveTurnToSteerError(err)) {
28594
+ throw err;
28595
+ }
28596
+ return false;
28597
+ }
28598
+ }
28599
+ /**
28600
+ * Starts a new turn from a steering prompt when there is no live turn to
28601
+ * inject into, and returns as soon as that turn is running.
28602
+ *
28603
+ * Waits for any previous prompt to drain first, then re-checks that the
28604
+ * session is not closing — the await above is a window during which a close
28605
+ * request can arrive.
28606
+ *
28607
+ * @param params The target session id and the prompt to steer with.
28608
+ * @returns "startedNewTurn" once the turn is running; throws if the prompt
28609
+ * fails or is cancelled before the turn starts.
28610
+ */
28611
+ async startNewTurnFromSteering(params) {
28612
+ const previousPrompt = this.activePrompts.get(params.sessionId);
28613
+ await previousPrompt?.completion;
28614
+ if (this.sessionIsClosing(params.sessionId)) {
28615
+ throw RequestError.invalidRequest(`Session ${params.sessionId} is closing`);
28616
+ }
28617
+ return await new Promise((resolve, reject) => {
28618
+ let turnStarted = false;
28619
+ const promptDone = this.prompt(params, void 0, () => {
28620
+ turnStarted = true;
28621
+ logger.log("Steering session started a new turn", { sessionId: params.sessionId });
28622
+ resolve({ outcome: "startedNewTurn" });
28623
+ });
28624
+ promptDone.then(
28625
+ (response) => {
28626
+ if (!turnStarted && response.stopReason === "cancelled") {
28627
+ reject(RequestError.invalidRequest(`Session ${params.sessionId} was cancelled before the steering turn started`));
28628
+ } else {
28629
+ resolve({ outcome: "startedNewTurn" });
28630
+ }
28631
+ },
28632
+ (error48) => {
28633
+ if (turnStarted) {
28634
+ logger.error(`Steering-started prompt for session ${params.sessionId} failed`, error48);
28635
+ } else {
28636
+ reject(error48);
28637
+ }
28638
+ }
28639
+ );
28640
+ });
28641
+ }
28642
+ isNoActiveTurnToSteerError(error48) {
28643
+ const messages = error48 instanceof Error ? [error48.message] : [];
28644
+ if (typeof error48 === "object" && error48 !== null && "data" in error48) {
28645
+ const data = error48.data;
28646
+ if (typeof data === "string") {
28647
+ messages.push(data);
28648
+ } else if (typeof data === "object" && data !== null && "details" in data) {
28649
+ const details = data.details;
28650
+ if (typeof details === "string") {
28651
+ messages.push(details);
28652
+ }
28653
+ }
28654
+ }
28655
+ return messages.some((message) => message.toLowerCase().includes("no active turn to steer"));
28656
+ }
28657
+ async getSteerableTurnId(sessionState) {
28658
+ if (this.sessionIsClosing(sessionState.sessionId)) {
28659
+ return null;
28660
+ }
28661
+ if (sessionState.currentTurnId) {
28662
+ return sessionState.currentTurnId;
28663
+ }
28664
+ const pendingTurnStart = this.pendingTurnStarts.get(sessionState.sessionId);
28665
+ if (!pendingTurnStart) {
28666
+ return null;
28667
+ }
28668
+ return await pendingTurnStart.promise;
28669
+ }
28670
+ parseSessionSteerParams(params) {
28671
+ const sessionId = params["sessionId"];
28672
+ const prompt = params["prompt"];
28673
+ const steerId = params["steerId"];
28674
+ if (typeof sessionId !== "string" || !Array.isArray(prompt) || steerId !== void 0 && (typeof steerId !== "string" || steerId.length === 0)) {
28675
+ throw RequestError.invalidParams();
28676
+ }
28677
+ return {
28678
+ sessionId,
28679
+ prompt,
28680
+ ...typeof steerId === "string" ? { steerId } : {}
28681
+ };
28682
+ }
27777
28683
  createSessionConfigOptions(sessionState) {
27778
28684
  const currentModelId = ModelId.fromString(sessionState.currentModelId);
27779
28685
  const configOptions = [
27780
28686
  sessionState.agentMode.toConfigOption(),
28687
+ createCollaborationModeConfigOption(sessionState.collaborationMode),
27781
28688
  createModelConfigOption(sessionState.availableModels, currentModelId.model)
27782
28689
  ];
27783
28690
  if (sessionState.supportedReasoningEfforts.length > 0) {
@@ -27791,7 +28698,6 @@ You have been logged out. Please try again.`);
27791
28698
  this.booleanConfigOptionsSupported
27792
28699
  ));
27793
28700
  }
27794
- configOptions.push(createPlanModeConfigOption(sessionState.planModeEnabled));
27795
28701
  return configOptions;
27796
28702
  }
27797
28703
  createSessionConfigOptionsResponse(sessionState) {
@@ -27805,18 +28711,45 @@ You have been logged out. Please try again.`);
27805
28711
  isSessionConfigEnabled() {
27806
28712
  return !isJetBrains2026_1Client(this.clientInfo);
27807
28713
  }
27808
- createPlanModeCollaborationMode(sessionState, modelId) {
27809
- if (!sessionState.planModeEnabled && !sessionState.planModeExplicitlySet) {
27810
- return null;
28714
+ publishCurrentGoalAsync(sessionState, sessionGeneration) {
28715
+ void this.publishCurrentGoalBestEffort(sessionState, sessionGeneration, true);
28716
+ }
28717
+ async publishCurrentGoalBestEffort(sessionState, sessionGeneration, force) {
28718
+ try {
28719
+ await this.publishCurrentGoal(sessionState, sessionGeneration, force);
28720
+ } catch (err) {
28721
+ logger.error(`Failed to publish current goal for session ${sessionState.sessionId}`, err);
27811
28722
  }
27812
- return {
27813
- mode: sessionState.planModeEnabled ? "plan" : "default",
27814
- settings: {
27815
- model: modelId.model,
27816
- reasoning_effort: modelId.effort,
27817
- developer_instructions: null
28723
+ }
28724
+ async publishCurrentGoal(sessionState, sessionGeneration, force) {
28725
+ const requestRevision = ++sessionState.goalRevision;
28726
+ const goal = await this.runWithProcessCheck(() => this.codexAcpClient.getGoal(sessionState.sessionId));
28727
+ const snapshot = goal === null ? null : toThreadGoalSnapshot(goal);
28728
+ if (!this.goalPublishIsCurrent(sessionState, sessionGeneration) || sessionState.goalRevision !== requestRevision) {
28729
+ return;
28730
+ }
28731
+ await this.publishGoalSnapshot(sessionState, snapshot, force, false);
28732
+ }
28733
+ goalPublishIsCurrent(sessionState, sessionGeneration) {
28734
+ return this.sessions.get(sessionState.sessionId) === sessionState && this.getSessionGeneration(sessionState.sessionId) === sessionGeneration && !this.sessionIsClosing(sessionState.sessionId);
28735
+ }
28736
+ async publishGoalSnapshot(sessionState, snapshot, force, incrementRevision = true) {
28737
+ if (incrementRevision) {
28738
+ sessionState.goalRevision += 1;
28739
+ }
28740
+ if (!force && sameThreadGoalSnapshot(sessionState.currentGoal, snapshot)) {
28741
+ return;
28742
+ }
28743
+ sessionState.currentGoal = snapshot;
28744
+ const session = new ACPSessionConnection(this.connection, sessionState.sessionId);
28745
+ await session.update({
28746
+ sessionUpdate: "session_info_update",
28747
+ _meta: {
28748
+ codex: {
28749
+ goal: snapshot
28750
+ }
27818
28751
  }
27819
- };
28752
+ });
27820
28753
  }
27821
28754
  findCurrentModel(models, currentModelId) {
27822
28755
  const modelId = ModelId.fromString(currentModelId);
@@ -27883,6 +28816,7 @@ You have been logged out. Please try again.`);
27883
28816
  supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [],
27884
28817
  supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"],
27885
28818
  agentMode: AgentMode.getInitialAgentMode(),
28819
+ collaborationMode: sessionMetadata.collaborationMode,
27886
28820
  currentTurnId: null,
27887
28821
  lastTokenUsage: null,
27888
28822
  totalTokenUsage: null,
@@ -27895,10 +28829,9 @@ You have been logged out. Please try again.`);
27895
28829
  additionalDirectories: sessionMetadata.additionalDirectories,
27896
28830
  fastModeEnabled: sessionMetadata.currentServiceTier === "fast",
27897
28831
  currentModelSupportsFast,
27898
- planModeEnabled: false,
27899
- planModeExplicitlySet: false,
27900
28832
  sessionMcpServers,
27901
28833
  terminalOutputMode: this.terminalOutputMode,
28834
+ goalRevision: 0,
27902
28835
  sessionTitle: null,
27903
28836
  sessionTitleSource: "unset"
27904
28837
  };
@@ -27913,6 +28846,7 @@ You have been logged out. Please try again.`);
27913
28846
  this.publishMcpStartupStatusAsync(sessionId);
27914
28847
  }
27915
28848
  const availableCommands = await this.availableCommands.getAvailableCommands(sessionState);
28849
+ await this.publishCurrentGoalBestEffort(sessionState, requestedSessionGeneration, true);
27916
28850
  const sessionModelState = this.createModelState(models, currentModelId);
27917
28851
  const sessionModeState = sessionState.agentMode.toSessionModeState();
27918
28852
  return {
@@ -27989,9 +28923,10 @@ You have been logged out. Please try again.`);
27989
28923
  case "userMessage":
27990
28924
  return this.createUserMessageUpdates(item);
27991
28925
  case "hookPrompt":
27992
- case "subAgentActivity":
27993
28926
  case "sleep":
27994
28927
  return [];
28928
+ case "subAgentActivity":
28929
+ return [createSubAgentActivityUpdate(item, "completed", "tool_call")];
27995
28930
  case "agentMessage": {
27996
28931
  return [{
27997
28932
  sessionUpdate: "agent_message_chunk",
@@ -28031,7 +28966,7 @@ You have been logged out. Please try again.`);
28031
28966
  case "contextCompaction":
28032
28967
  return [createCompletedContextCompactionUpdate(item)];
28033
28968
  case "plan":
28034
- return [this.createPlanUpdate(item)];
28969
+ return item.text.length > 0 ? [this.createPlanHistoryUpdate(item)] : [];
28035
28970
  }
28036
28971
  }
28037
28972
  createUserMessageUpdates(item) {
@@ -28071,15 +29006,22 @@ You have been logged out. Please try again.`);
28071
29006
  }
28072
29007
  };
28073
29008
  }
28074
- createPlanUpdate(item) {
28075
- return {
28076
- sessionUpdate: "agent_message_chunk",
28077
- content: {
28078
- type: "text",
28079
- text: `Plan:
28080
- ${item.text}`
28081
- }
28082
- };
29009
+ createPlanHistoryUpdate(item) {
29010
+ if (clientSupportsPlanUpdates(this.clientCapabilities)) {
29011
+ return {
29012
+ sessionUpdate: "plan_update",
29013
+ plan: {
29014
+ type: "markdown",
29015
+ planId: item.id,
29016
+ content: item.text
29017
+ }
29018
+ };
29019
+ }
29020
+ return createAgentTextMessageChunk(
29021
+ item.text,
29022
+ item.id,
29023
+ createCodexMessagePhaseMeta("final_answer")
29024
+ );
28083
29025
  }
28084
29026
  userInputToContentBlocks(input) {
28085
29027
  switch (input.type) {
@@ -28190,7 +29132,6 @@ ${item.text}`
28190
29132
  cancelSignal,
28191
29133
  signal: abortController.signal,
28192
29134
  currentTurn: null,
28193
- outcome: null,
28194
29135
  requestCancel: () => {
28195
29136
  if (abortController.signal.aborted) {
28196
29137
  return;
@@ -28241,51 +29182,6 @@ ${item.text}`
28241
29182
  if (pending.size === 0) this.pendingSteers.delete(sessionId);
28242
29183
  await this.connection.notify(CODEX_STEER_APPLIED_METHOD, { sessionId, steerId });
28243
29184
  }
28244
- async steerPrompt(params) {
28245
- const steerId = getCodexSteerId(params._meta);
28246
- if (!steerId) throw RequestError.invalidRequest("Missing Codex steer id");
28247
- const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
28248
- if (firstText.startsWith("/")) {
28249
- throw RequestError.invalidRequest("Slash commands cannot steer an active Codex turn");
28250
- }
28251
- let activePrompt = this.activePrompts.get(params.sessionId);
28252
- if (!activePrompt) throw RequestError.invalidRequest("No active Codex turn to steer");
28253
- if (!activePrompt.currentTurn) {
28254
- await this.pendingTurnStarts.get(params.sessionId)?.promise;
28255
- activePrompt = this.activePrompts.get(params.sessionId);
28256
- }
28257
- const turn = activePrompt?.currentTurn;
28258
- if (!activePrompt || !turn || activePrompt.signal.aborted) {
28259
- throw RequestError.invalidRequest("No active Codex turn to steer");
28260
- }
28261
- const pending = this.pendingSteers.get(params.sessionId) ?? /* @__PURE__ */ new Map();
28262
- if (pending.has(steerId)) {
28263
- throw RequestError.invalidRequest(`Duplicate Codex steer id: ${steerId}`);
28264
- }
28265
- pending.set(steerId, { activePrompt, turnId: turn.turnId });
28266
- this.pendingSteers.set(params.sessionId, pending);
28267
- try {
28268
- const steeredTurnId = await this.runWithProcessCheck(
28269
- () => this.codexAcpClient.sendSteer(params, turn.turnId, steerId)
28270
- );
28271
- if (steeredTurnId !== turn.turnId) {
28272
- throw RequestError.internalError(
28273
- void 0,
28274
- `Codex steered unexpected turn ${steeredTurnId}; expected ${turn.turnId}`
28275
- );
28276
- }
28277
- } catch (error48) {
28278
- if (pending.get(steerId)?.activePrompt === activePrompt) {
28279
- pending.delete(steerId);
28280
- if (pending.size === 0) this.pendingSteers.delete(params.sessionId);
28281
- }
28282
- throw error48;
28283
- }
28284
- if (!activePrompt.outcome) {
28285
- throw RequestError.internalError(void 0, "Active Codex prompt has no tracked outcome");
28286
- }
28287
- return await activePrompt.outcome;
28288
- }
28289
29185
  cancelBeforeTurnStarted(activePrompt) {
28290
29186
  return activePrompt.cancelSignal.then(() => {
28291
29187
  if (activePrompt.currentTurn === null) {
@@ -28363,43 +29259,40 @@ ${item.text}`
28363
29259
  return activePrompt.signal.aborted || this.activePrompts.get(sessionId) !== activePrompt || this.sessionIsClosing(sessionId);
28364
29260
  }
28365
29261
  async interruptSessionTurn(sessionState, requestName, resolveInterruptedTurn) {
28366
- const turnId = await this.getInterruptibleTurnId(sessionState, requestName);
28367
- if (!turnId) {
29262
+ const turn = await this.getInterruptibleTurn(sessionState, requestName);
29263
+ if (!turn) {
28368
29264
  return;
28369
29265
  }
28370
29266
  logger.log(`${requestName} session requested`, {
28371
29267
  sessionId: sessionState.sessionId,
28372
- currentTurnId: turnId
29268
+ threadId: turn.threadId,
29269
+ currentTurnId: turn.turnId
28373
29270
  });
28374
29271
  if (resolveInterruptedTurn) {
28375
- this.codexAcpClient.markTurnStale({
28376
- threadId: sessionState.sessionId,
28377
- turnId
28378
- });
29272
+ this.codexAcpClient.markTurnStale(turn);
28379
29273
  }
28380
29274
  try {
28381
- await this.runWithProcessCheck(() => this.codexAcpClient.turnInterrupt({
28382
- threadId: sessionState.sessionId,
28383
- turnId
28384
- }));
29275
+ await this.runWithProcessCheck(() => this.codexAcpClient.turnInterrupt(turn));
28385
29276
  logger.log(`${requestName} - turnInterrupt succeeded`, {
28386
29277
  sessionId: sessionState.sessionId,
28387
- currentTurnId: turnId
29278
+ threadId: turn.threadId,
29279
+ currentTurnId: turn.turnId
28388
29280
  });
28389
29281
  } catch (err) {
28390
29282
  logger.error(`${requestName} - turnInterrupt failed`, err);
28391
29283
  } finally {
28392
29284
  if (resolveInterruptedTurn) {
28393
- this.codexAcpClient.resolveTurnInterrupted({
28394
- threadId: sessionState.sessionId,
28395
- turnId
28396
- });
29285
+ this.codexAcpClient.resolveTurnInterrupted(turn);
28397
29286
  }
28398
29287
  }
28399
29288
  }
28400
- async getInterruptibleTurnId(sessionState, requestName) {
29289
+ async getInterruptibleTurn(sessionState, requestName) {
29290
+ const currentTurn = this.activePrompts.get(sessionState.sessionId)?.currentTurn;
29291
+ if (currentTurn) {
29292
+ return currentTurn;
29293
+ }
28401
29294
  if (sessionState.currentTurnId) {
28402
- return sessionState.currentTurnId;
29295
+ return { threadId: sessionState.sessionId, turnId: sessionState.currentTurnId };
28403
29296
  }
28404
29297
  const pendingTurnStart = this.pendingTurnStarts.get(sessionState.sessionId);
28405
29298
  if (!pendingTurnStart) {
@@ -28413,26 +29306,17 @@ ${item.text}`
28413
29306
  const turnId = await pendingTurnStart.promise;
28414
29307
  if (!turnId) {
28415
29308
  logger.log(`${requestName} request rejected: no current turn`, { sessionId: sessionState.sessionId });
29309
+ return null;
28416
29310
  }
28417
- return turnId;
29311
+ const startedTurn = this.activePrompts.get(sessionState.sessionId)?.currentTurn;
29312
+ return startedTurn ?? { threadId: sessionState.sessionId, turnId };
28418
29313
  }
28419
- async prompt(params, signal) {
28420
- if (getCodexSteerId(params._meta)) {
28421
- return await this.steerPrompt(params);
28422
- }
29314
+ async prompt(params, signal, onTurnStarted) {
28423
29315
  if (this.activePrompts.has(params.sessionId)) {
28424
29316
  throw RequestError.invalidRequest(
28425
29317
  "A Codex prompt is already active; use the advertised steer extension"
28426
29318
  );
28427
29319
  }
28428
- const outcome = this.runPrompt(params, signal);
28429
- const activePrompt = this.activePrompts.get(params.sessionId);
28430
- if (activePrompt) {
28431
- activePrompt.outcome = outcome;
28432
- }
28433
- return await outcome;
28434
- }
28435
- async runPrompt(params, signal) {
28436
29320
  logger.log("Prompt received", {
28437
29321
  sessionId: params.sessionId,
28438
29322
  prompt: params.prompt
@@ -28441,11 +29325,23 @@ ${item.text}`
28441
29325
  sessionState.currentTurnId = null;
28442
29326
  sessionState.lastTokenUsage = null;
28443
29327
  const activePrompt = this.trackActivePrompt(params.sessionId);
28444
- const pendingTurnStart = this.createPendingTurnStart();
28445
- this.pendingTurnStarts.set(params.sessionId, pendingTurnStart);
29328
+ let pendingTurnStart = null;
29329
+ const ensurePendingTurnStart = () => {
29330
+ if (pendingTurnStart === null) {
29331
+ pendingTurnStart = this.createPendingTurnStart();
29332
+ this.pendingTurnStarts.set(params.sessionId, pendingTurnStart);
29333
+ }
29334
+ return pendingTurnStart;
29335
+ };
28446
29336
  const disposePromptRequestCancellation = this.observePromptRequestCancellation(signal, sessionState, activePrompt);
29337
+ let eventHandler = null;
28447
29338
  try {
28448
- const eventHandler = new CodexEventHandler(this.connection, sessionState);
29339
+ const promptEventHandler = new CodexEventHandler(
29340
+ this.connection,
29341
+ sessionState,
29342
+ clientSupportsPlanUpdates(this.clientCapabilities)
29343
+ );
29344
+ eventHandler = promptEventHandler;
28449
29345
  const approvalHandler = new CodexApprovalHandler(this.connection, sessionState, activePrompt.signal);
28450
29346
  const elicitationHandler = new CodexElicitationHandler(
28451
29347
  this.connection,
@@ -28458,7 +29354,7 @@ ${item.text}`
28458
29354
  async (event) => {
28459
29355
  await this.handleSteerAppliedNotification(params.sessionId, event, activePrompt);
28460
29356
  await elicitationHandler.handleNotification(event);
28461
- return eventHandler.handleNotification(event);
29357
+ return promptEventHandler.handleNotification(event);
28462
29358
  },
28463
29359
  approvalHandler,
28464
29360
  elicitationHandler
@@ -28467,6 +29363,9 @@ ${item.text}`
28467
29363
  return this.cancelledPromptResponse(sessionState);
28468
29364
  }
28469
29365
  const commandPromise = this.availableCommands.tryHandleCommand(params.prompt, sessionState, {
29366
+ onTurnStartPending: () => {
29367
+ ensurePendingTurnStart();
29368
+ },
28470
29369
  onTurnStarted: (turnId, threadId) => {
28471
29370
  const turn = { threadId, turnId };
28472
29371
  activePrompt.currentTurn = turn;
@@ -28475,7 +29374,20 @@ ${item.text}`
28475
29374
  return;
28476
29375
  }
28477
29376
  sessionState.currentTurnId = turnId;
28478
- pendingTurnStart.resolve(turnId);
29377
+ pendingTurnStart?.resolve(turnId);
29378
+ onTurnStarted?.();
29379
+ },
29380
+ setConfigOption: async (configId, value) => {
29381
+ await this.applySessionConfigOption(sessionState, {
29382
+ sessionId: sessionState.sessionId,
29383
+ configId,
29384
+ value
29385
+ });
29386
+ const session = new ACPSessionConnection(this.connection, sessionState.sessionId);
29387
+ await session.update({
29388
+ sessionUpdate: "config_option_update",
29389
+ configOptions: this.createSessionConfigOptions(sessionState)
29390
+ });
28479
29391
  }
28480
29392
  });
28481
29393
  void commandPromise.catch((err) => {
@@ -28495,7 +29407,6 @@ ${item.text}`
28495
29407
  logger.log("Prompt handled by a command");
28496
29408
  await this.codexAcpClient.waitForSessionNotifications(params.sessionId);
28497
29409
  if (commandResult.turnCompleted?.turn.status === "interrupted") {
28498
- await this.notifyConversationInterrupted(params.sessionId);
28499
29410
  return this.cancelledPromptResponse(sessionState);
28500
29411
  }
28501
29412
  const error49 = eventHandler.getFailure();
@@ -28528,14 +29439,13 @@ ${item.text}`
28528
29439
  sessionState.fastModeEnabled,
28529
29440
  sessionState.currentModelSupportsFast
28530
29441
  );
28531
- const collaborationMode = this.createPlanModeCollaborationMode(sessionState, modelId);
29442
+ ensurePendingTurnStart();
28532
29443
  const sendPromptPromise = this.runWithProcessCheck(
28533
29444
  () => this.codexAcpClient.sendPrompt(
28534
29445
  params,
28535
29446
  agentMode,
28536
29447
  modelId,
28537
29448
  serviceTier,
28538
- collaborationMode,
28539
29449
  disableSummary,
28540
29450
  sessionState.cwd,
28541
29451
  sessionState.additionalDirectories,
@@ -28547,7 +29457,8 @@ ${item.text}`
28547
29457
  return;
28548
29458
  }
28549
29459
  sessionState.currentTurnId = turnId;
28550
- pendingTurnStart.resolve(turnId);
29460
+ pendingTurnStart?.resolve(turnId);
29461
+ onTurnStarted?.();
28551
29462
  },
28552
29463
  () => this.promptShouldStop(params.sessionId, activePrompt)
28553
29464
  )
@@ -28557,7 +29468,7 @@ ${item.text}`
28557
29468
  logger.error(`Prompt for cancelled session ${params.sessionId} failed after prompt returned`, err);
28558
29469
  }
28559
29470
  });
28560
- const turnCompleted = await Promise.race([
29471
+ let turnCompleted = await Promise.race([
28561
29472
  sendPromptPromise,
28562
29473
  activePrompt.closeSignal,
28563
29474
  this.cancelBeforeTurnStarted(activePrompt)
@@ -28567,13 +29478,81 @@ ${item.text}`
28567
29478
  }
28568
29479
  await this.codexAcpClient.waitForSessionNotifications(params.sessionId);
28569
29480
  if (turnCompleted.turn.status === "interrupted") {
28570
- await this.notifyConversationInterrupted(params.sessionId);
29481
+ await eventHandler.flushPendingPlanUpdates();
28571
29482
  return this.cancelledPromptResponse(sessionState);
28572
29483
  }
28573
29484
  const error48 = eventHandler.getFailure();
28574
29485
  if (error48) {
28575
29486
  throw error48;
28576
29487
  }
29488
+ await eventHandler.flushPendingPlanUpdates();
29489
+ const completedPlan = eventHandler.takeCompletedPlan();
29490
+ if (completedPlan !== null && sessionState.collaborationMode === PLAN_COLLABORATION_MODE && !this.promptShouldStop(params.sessionId, activePrompt)) {
29491
+ const approved = await this.requestPlanImplementationPermission(
29492
+ sessionState,
29493
+ completedPlan,
29494
+ activePrompt.signal
29495
+ );
29496
+ if (this.promptShouldStop(params.sessionId, activePrompt)) {
29497
+ return this.cancelledPromptResponse(sessionState);
29498
+ }
29499
+ if (approved && !this.promptShouldStop(params.sessionId, activePrompt)) {
29500
+ await this.applyCollaborationModeChange(sessionState, DEFAULT_COLLABORATION_MODE);
29501
+ const session = new ACPSessionConnection(this.connection, sessionState.sessionId);
29502
+ await session.update({
29503
+ sessionUpdate: "config_option_update",
29504
+ configOptions: this.createSessionConfigOptions(sessionState)
29505
+ });
29506
+ const implementationRequest = {
29507
+ sessionId: params.sessionId,
29508
+ prompt: [{ type: "text", text: "Implement the approved plan." }]
29509
+ };
29510
+ activePrompt.currentTurn = null;
29511
+ const implementationPromise = this.runWithProcessCheck(
29512
+ () => this.codexAcpClient.sendPrompt(
29513
+ implementationRequest,
29514
+ agentMode,
29515
+ modelId,
29516
+ serviceTier,
29517
+ disableSummary,
29518
+ sessionState.cwd,
29519
+ sessionState.additionalDirectories,
29520
+ (turnId) => {
29521
+ const turn = { threadId: params.sessionId, turnId };
29522
+ activePrompt.currentTurn = turn;
29523
+ if (this.promptShouldStop(params.sessionId, activePrompt)) {
29524
+ this.interruptLateStartedTurn(turn);
29525
+ return;
29526
+ }
29527
+ sessionState.currentTurnId = turnId;
29528
+ },
29529
+ () => this.promptShouldStop(params.sessionId, activePrompt)
29530
+ )
29531
+ );
29532
+ void implementationPromise.catch((err) => {
29533
+ if (this.activePrompts.get(params.sessionId) !== activePrompt) {
29534
+ logger.error(`Implementation turn for cancelled prompt ${params.sessionId} failed after prompt returned`, err);
29535
+ }
29536
+ });
29537
+ turnCompleted = await Promise.race([
29538
+ implementationPromise,
29539
+ activePrompt.closeSignal,
29540
+ this.cancelBeforeTurnStarted(activePrompt)
29541
+ ]);
29542
+ if (turnCompleted === null) {
29543
+ return this.cancelledPromptResponse(sessionState);
29544
+ }
29545
+ await this.codexAcpClient.waitForSessionNotifications(params.sessionId);
29546
+ if (turnCompleted.turn.status === "interrupted") {
29547
+ await eventHandler.flushPendingPlanUpdates();
29548
+ return this.cancelledPromptResponse(sessionState);
29549
+ }
29550
+ const implementationError = eventHandler.getFailure();
29551
+ if (implementationError) {
29552
+ throw implementationError;
29553
+ }
29554
+ }
29555
+ }
28577
29556
  await this.publishFallbackSessionTitle(
28578
29557
  sessionState,
28579
29558
  this.createPromptFallbackTitle(params.prompt)
@@ -28588,6 +29567,7 @@ ${item.text}`
28588
29567
  throw err;
28589
29568
  } finally {
28590
29569
  logger.log("Prompt completed", { sessionId: params.sessionId });
29570
+ await eventHandler?.dispose();
28591
29571
  disposePromptRequestCancellation();
28592
29572
  sessionState.currentTurnId = null;
28593
29573
  const registeredPendingTurnStart = this.pendingTurnStarts.get(params.sessionId);
@@ -28598,6 +29578,57 @@ ${item.text}`
28598
29578
  activePrompt.complete();
28599
29579
  }
28600
29580
  }
29581
+ async requestPlanImplementationPermission(sessionState, plan, cancellationSignal) {
29582
+ const toolCallId = `plan-review:${plan.itemId}`;
29583
+ try {
29584
+ const response = await this.connection.request(
29585
+ methods.client.session.requestPermission,
29586
+ {
29587
+ sessionId: sessionState.sessionId,
29588
+ toolCall: {
29589
+ toolCallId,
29590
+ title: "Implement this plan?",
29591
+ kind: "switch_mode",
29592
+ status: "pending",
29593
+ rawInput: { plan: plan.text }
29594
+ },
29595
+ options: [
29596
+ {
29597
+ optionId: IMPLEMENT_PLAN_OPTION_ID,
29598
+ name: "Yes, implement this plan",
29599
+ kind: "allow_once"
29600
+ },
29601
+ {
29602
+ optionId: REVISE_PLAN_OPTION_ID,
29603
+ name: "No, and tell Codex what to do differently",
29604
+ kind: "reject_once"
29605
+ }
29606
+ ],
29607
+ _meta: {
29608
+ codex: {
29609
+ kind: "plan_review",
29610
+ planItemId: plan.itemId
29611
+ }
29612
+ }
29613
+ },
29614
+ { cancellationSignal }
29615
+ );
29616
+ const approved = response.outcome.outcome === "selected" && response.outcome.optionId === IMPLEMENT_PLAN_OPTION_ID;
29617
+ await this.connection.notify(methods.client.session.update, {
29618
+ sessionId: sessionState.sessionId,
29619
+ update: {
29620
+ sessionUpdate: "tool_call_update",
29621
+ toolCallId,
29622
+ status: "completed",
29623
+ rawOutput: approved ? "User approved the plan." : "User kept the session in plan mode."
29624
+ }
29625
+ });
29626
+ return approved;
29627
+ } catch (error48) {
29628
+ logger.error("Error requesting plan implementation permission", error48);
29629
+ return false;
29630
+ }
29631
+ }
28601
29632
  cancelledPromptResponse(sessionState) {
28602
29633
  return {
28603
29634
  stopReason: "cancelled",
@@ -28605,15 +29636,6 @@ ${item.text}`
28605
29636
  _meta: this.buildQuotaMeta(sessionState)
28606
29637
  };
28607
29638
  }
28608
- async notifyConversationInterrupted(sessionId) {
28609
- if (this.sessionIsClosing(sessionId) || !this.sessions.has(sessionId)) {
28610
- return;
28611
- }
28612
- await this.connection.notify(methods.client.session.update, {
28613
- sessionId,
28614
- update: createAgentTextMessageChunk("*Conversation interrupted*")
28615
- });
28616
- }
28617
29639
  buildQuotaMeta(sessionState) {
28618
29640
  const lastTokenUsage = sessionState.lastTokenUsage;
28619
29641
  const modelName = sessionState.currentModelId.replace(/\[.*?]$/, "");
@@ -28746,9 +29768,16 @@ var CodexAppServerClient = class {
28746
29768
  threadStatusCaptures = /* @__PURE__ */ new Map();
28747
29769
  threadGoalUpdateCaptures = /* @__PURE__ */ new Map();
28748
29770
  threadGoalClearedCaptures = /* @__PURE__ */ new Map();
29771
+ threadSettings = /* @__PURE__ */ new Map();
28749
29772
  staleTurnIds = /* @__PURE__ */ new Map();
29773
+ turnCompletionTerminalError = null;
28750
29774
  constructor(connection) {
28751
29775
  this.connection = connection;
29776
+ const failPendingTurns = () => this.rejectAllPendingTurnCompletions(
29777
+ new Error("Codex process exited before completing the turn")
29778
+ );
29779
+ this.connection.onClose(failPendingTurns);
29780
+ this.connection.onDispose(failPendingTurns);
28752
29781
  this.connection.onUnhandledNotification((data) => {
28753
29782
  const serverNotification = data;
28754
29783
  if (isMcpServerStatusUpdatedNotification(serverNotification)) {
@@ -28775,6 +29804,9 @@ var CodexAppServerClient = class {
28775
29804
  if (isThreadGoalClearedNotification(serverNotification)) {
28776
29805
  this.recordThreadGoalCleared(serverNotification.params);
28777
29806
  }
29807
+ if (serverNotification.method === "thread/settings/updated") {
29808
+ this.threadSettings.set(serverNotification.params.threadId, serverNotification.params.threadSettings);
29809
+ }
28778
29810
  const routing = extractTurnRouting(serverNotification);
28779
29811
  if (this.handleStaleTurnNotification(serverNotification, routing)) {
28780
29812
  return;
@@ -28856,9 +29888,6 @@ var CodexAppServerClient = class {
28856
29888
  async turnStart(params) {
28857
29889
  return await this.sendRequest({ method: "turn/start", params });
28858
29890
  }
28859
- async turnSteer(params) {
28860
- return await this.sendRequest({ method: "turn/steer", params });
28861
- }
28862
29891
  async runTurn(params, onTurnStarted) {
28863
29892
  const capturedCompletions = [];
28864
29893
  const releaseCapture = this.captureTurnCompletions(params.threadId, (event) => {
@@ -28868,11 +29897,12 @@ var CodexAppServerClient = class {
28868
29897
  const turnStarted = await this.turnStart(params);
28869
29898
  onTurnStarted?.(turnStarted.turn.id);
28870
29899
  const earlyCompletion = capturedCompletions.find((event) => event.turn.id === turnStarted.turn.id);
28871
- releaseCapture();
28872
29900
  if (earlyCompletion) {
28873
29901
  return earlyCompletion;
28874
29902
  }
28875
- return await this.awaitTurnCompleted(params.threadId, turnStarted.turn.id);
29903
+ const completion = this.awaitTurnCompleted(params.threadId, turnStarted.turn.id);
29904
+ releaseCapture();
29905
+ return await completion;
28876
29906
  } finally {
28877
29907
  releaseCapture();
28878
29908
  }
@@ -28886,16 +29916,17 @@ var CodexAppServerClient = class {
28886
29916
  const reviewStarted = await this.reviewStart(params);
28887
29917
  onTurnStarted?.(reviewStarted.turn.id, reviewStarted.reviewThreadId);
28888
29918
  const earlyCompletion = capturedCompletions.find((event) => event.turn.id === reviewStarted.turn.id);
28889
- releaseCapture();
28890
29919
  if (earlyCompletion) {
28891
29920
  return earlyCompletion;
28892
29921
  }
28893
- return await this.awaitTurnCompleted(reviewStarted.reviewThreadId, reviewStarted.turn.id);
29922
+ const completion = this.awaitTurnCompleted(reviewStarted.reviewThreadId, reviewStarted.turn.id);
29923
+ releaseCapture();
29924
+ return await completion;
28894
29925
  } finally {
28895
29926
  releaseCapture();
28896
29927
  }
28897
29928
  }
28898
- async runGoalSet(params, onTurnStarted, runtimeEffectsGraceMs = GOAL_RUNTIME_EFFECTS_GRACE_MS) {
29929
+ async runGoalSet(params, onTurnStarted, runtimeEffectsGraceMs = GOAL_RUNTIME_EFFECTS_GRACE_MS, onGoalSet) {
28899
29930
  let goalTurnId = null;
28900
29931
  const capturedCompletions = [];
28901
29932
  let resolveGoalTurnCompleted = () => {
@@ -28948,6 +29979,7 @@ var CodexAppServerClient = class {
28948
29979
  try {
28949
29980
  const goalSetResponse = await this.threadGoalSet(params);
28950
29981
  expectedGoal = goalSetResponse.goal;
29982
+ onGoalSet?.(expectedGoal);
28951
29983
  if (capturedGoalUpdates.some((event) => goalsMatch(event.goal, expectedGoal))) {
28952
29984
  goalUpdateHandled = true;
28953
29985
  resolveGoalUpdateHandled();
@@ -29071,6 +30103,9 @@ var CodexAppServerClient = class {
29071
30103
  async turnInterrupt(params) {
29072
30104
  return await this.sendRequest({ method: "turn/interrupt", params });
29073
30105
  }
30106
+ async turnSteer(params) {
30107
+ return await this.sendRequest({ method: "turn/steer", params });
30108
+ }
29074
30109
  async reviewStart(params) {
29075
30110
  return await this.sendRequest({ method: "review/start", params });
29076
30111
  }
@@ -29088,6 +30123,12 @@ var CodexAppServerClient = class {
29088
30123
  async threadFork(params) {
29089
30124
  return await this.sendRequest({ method: "thread/fork", params });
29090
30125
  }
30126
+ getThreadSettings(threadId) {
30127
+ return this.threadSettings.get(threadId);
30128
+ }
30129
+ async threadSettingsUpdate(params) {
30130
+ await this.connection.sendRequest("thread/settings/update", params);
30131
+ }
29091
30132
  async threadList(params) {
29092
30133
  return await this.sendRequest({ method: "thread/list", params });
29093
30134
  }
@@ -29109,6 +30150,9 @@ var CodexAppServerClient = class {
29109
30150
  async threadGoalSet(params) {
29110
30151
  return await this.sendRequest({ method: "thread/goal/set", params });
29111
30152
  }
30153
+ async threadGoalGet(params) {
30154
+ return await this.sendRequest({ method: "thread/goal/get", params });
30155
+ }
29112
30156
  async threadGoalClear(params) {
29113
30157
  return await this.sendRequest({ method: "thread/goal/clear", params });
29114
30158
  }
@@ -29152,9 +30196,12 @@ var CodexAppServerClient = class {
29152
30196
  }
29153
30197
  //TODO create type-safe helper
29154
30198
  async awaitTurnCompleted(threadId, turnId) {
29155
- return await new Promise((resolve) => {
30199
+ if (this.turnCompletionTerminalError) {
30200
+ throw this.turnCompletionTerminalError;
30201
+ }
30202
+ return await new Promise((resolve, reject) => {
29156
30203
  const threadResolvers = this.getOrCreatePendingTurnCompletionResolvers(threadId);
29157
- threadResolvers.set(turnId, resolve);
30204
+ threadResolvers.set(turnId, { resolve, reject });
29158
30205
  });
29159
30206
  }
29160
30207
  async awaitCompactionCompleted(threadId) {
@@ -29215,13 +30262,13 @@ var CodexAppServerClient = class {
29215
30262
  }
29216
30263
  recordTurnCompleted(event) {
29217
30264
  const threadResolvers = this.pendingTurnCompletionResolvers.get(event.threadId);
29218
- const resolve = threadResolvers?.get(event.turn.id);
29219
- if (resolve) {
30265
+ const entry = threadResolvers?.get(event.turn.id);
30266
+ if (entry) {
29220
30267
  threadResolvers.delete(event.turn.id);
29221
30268
  if (threadResolvers.size === 0) {
29222
30269
  this.pendingTurnCompletionResolvers.delete(event.threadId);
29223
30270
  }
29224
- resolve(event);
30271
+ entry.resolve(event);
29225
30272
  return;
29226
30273
  }
29227
30274
  const captures = this.turnCompletionCaptures.get(event.threadId);
@@ -29322,6 +30369,22 @@ var CodexAppServerClient = class {
29322
30369
  this.pendingTurnCompletionResolvers.set(threadId, created);
29323
30370
  return created;
29324
30371
  }
30372
+ /**
30373
+ * The codex process exiting mid-turn means `turn/completed` will never
30374
+ * arrive. Without this, `awaitTurnCompleted` hangs forever, the prompt
30375
+ * promise never settles, and the session rejects every future prompt with
30376
+ * "A Codex prompt is already active".
30377
+ */
30378
+ rejectAllPendingTurnCompletions(error48) {
30379
+ this.turnCompletionTerminalError ??= error48;
30380
+ const threads = [...this.pendingTurnCompletionResolvers.values()];
30381
+ this.pendingTurnCompletionResolvers.clear();
30382
+ for (const threadResolvers of threads) {
30383
+ for (const entry of threadResolvers.values()) {
30384
+ entry.reject(error48);
30385
+ }
30386
+ }
30387
+ }
29325
30388
  captureTurnCompletions(threadId, capture) {
29326
30389
  const captures = this.turnCompletionCaptures.get(threadId) ?? /* @__PURE__ */ new Set();
29327
30390
  captures.add(capture);
@@ -29633,6 +30696,15 @@ var legacySetSessionModelParamsParser = external_exports.object({
29633
30696
  sessionId: external_exports.string(),
29634
30697
  modelId: external_exports.string()
29635
30698
  }).passthrough();
30699
+ var sessionSteerParamsParser = external_exports.object({
30700
+ sessionId: external_exports.string(),
30701
+ prompt: external_exports.array(external_exports.any()),
30702
+ steerId: external_exports.string().min(1).optional()
30703
+ }).passthrough();
30704
+ var goalControlParamsParser = external_exports.object({
30705
+ sessionId: external_exports.string(),
30706
+ action: external_exports.enum(["pause", "clear"])
30707
+ }).passthrough();
29636
30708
  if (process.argv.includes("--version")) {
29637
30709
  console.log(`${package_default.name} ${package_default.version}`);
29638
30710
  process.exit(0);
@@ -29704,5 +30776,5 @@ function startAcpServer() {
29704
30776
  codexAcpServer = null;
29705
30777
  }
29706
30778
  });
29707
- }).onRequest(methods.agent.initialize, (ctx) => getAgent().initialize(ctx.params)).onRequest(methods.agent.session.new, (ctx) => getAgent().newSession(ctx.params)).onRequest(methods.agent.session.load, (ctx) => getAgent().loadSession(ctx.params)).onRequest(methods.agent.session.fork, (ctx) => getAgent().unstable_forkSession(ctx.params)).onRequest(methods.agent.session.list, (ctx) => getAgent().listSessions(ctx.params)).onRequest(methods.agent.session.delete, (ctx) => getAgent().deleteSession(ctx.params)).onRequest(methods.agent.session.resume, (ctx) => getAgent().resumeSession(ctx.params)).onRequest(methods.agent.session.close, (ctx) => getAgent().closeSession(ctx.params)).onRequest(methods.agent.session.setMode, (ctx) => getAgent().setSessionMode(ctx.params)).onRequest(methods.agent.session.setConfigOption, (ctx) => getAgent().setSessionConfigOption(ctx.params)).onRequest(methods.agent.authenticate, (ctx) => getAgent().authenticate(ctx.params)).onRequest(methods.agent.logout, (ctx) => getAgent().logout(ctx.params)).onRequest(methods.agent.providers.list, (ctx) => getAgent().listProviders(ctx.params)).onRequest(methods.agent.providers.set, (ctx) => getAgent().setProvider(ctx.params)).onRequest(methods.agent.providers.disable, (ctx) => getAgent().disableProvider(ctx.params)).onRequest(methods.agent.session.prompt, (ctx) => getAgent().prompt(ctx.params, ctx.signal)).onNotification(methods.agent.session.cancel, (ctx) => getAgent().cancel(ctx.params)).onRequest("authentication/status", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/status", ctx.params)).onRequest("authentication/logout", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/logout", ctx.params)).onRequest(LEGACY_SET_SESSION_MODEL_METHOD, legacySetSessionModelParamsParser, (ctx) => getAgent().extMethod(LEGACY_SET_SESSION_MODEL_METHOD, ctx.params)).connect(acpJsonStream);
30779
+ }).onRequest(methods.agent.initialize, (ctx) => getAgent().initialize(ctx.params)).onRequest(methods.agent.session.new, (ctx) => getAgent().newSession(ctx.params)).onRequest(methods.agent.session.load, (ctx) => getAgent().loadSession(ctx.params)).onRequest(methods.agent.session.fork, (ctx) => getAgent().unstable_forkSession(ctx.params)).onRequest(methods.agent.session.list, (ctx) => getAgent().listSessions(ctx.params)).onRequest(methods.agent.session.delete, (ctx) => getAgent().deleteSession(ctx.params)).onRequest(methods.agent.session.resume, (ctx) => getAgent().resumeSession(ctx.params)).onRequest(methods.agent.session.close, (ctx) => getAgent().closeSession(ctx.params)).onRequest(methods.agent.session.setMode, (ctx) => getAgent().setSessionMode(ctx.params)).onRequest(methods.agent.session.setConfigOption, (ctx) => getAgent().setSessionConfigOption(ctx.params)).onRequest(methods.agent.authenticate, (ctx) => getAgent().authenticate(ctx.params)).onRequest(methods.agent.logout, (ctx) => getAgent().logout(ctx.params)).onRequest(methods.agent.providers.list, (ctx) => getAgent().listProviders(ctx.params)).onRequest(methods.agent.providers.set, (ctx) => getAgent().setProvider(ctx.params)).onRequest(methods.agent.providers.disable, (ctx) => getAgent().disableProvider(ctx.params)).onRequest(methods.agent.session.prompt, (ctx) => getAgent().prompt(ctx.params, ctx.signal)).onNotification(methods.agent.session.cancel, (ctx) => getAgent().cancel(ctx.params)).onRequest("authentication/status", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/status", ctx.params)).onRequest("authentication/logout", emptyExtensionParamsParser, (ctx) => getAgent().extMethod("authentication/logout", ctx.params)).onRequest(LEGACY_SET_SESSION_MODEL_METHOD, legacySetSessionModelParamsParser, (ctx) => getAgent().extMethod(LEGACY_SET_SESSION_MODEL_METHOD, ctx.params)).onRequest(SESSION_STEERING_METHOD, sessionSteerParamsParser, (ctx) => getAgent().extMethod(SESSION_STEERING_METHOD, ctx.params)).onRequest(GOAL_CONTROL_METHOD, goalControlParamsParser, (ctx) => getAgent().extMethod(GOAL_CONTROL_METHOD, ctx.params)).connect(acpJsonStream);
29708
30780
  }