@agentclientprotocol/codex-acp 1.1.13 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +567 -99
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -3453,7 +3453,7 @@ var require_main = __commonJS({
3453
3453
  exports.createMessageConnection = createMessageConnection2;
3454
3454
  var ril_1 = __importDefault(require_ril());
3455
3455
  ril_1.default.install();
3456
- var path6 = __importStar(__require("path"));
3456
+ var path7 = __importStar(__require("path"));
3457
3457
  var os2 = __importStar(__require("os"));
3458
3458
  var fs7 = __importStar(__require("fs"));
3459
3459
  var crypto_1 = __require("crypto");
@@ -3605,7 +3605,7 @@ var require_main = __commonJS({
3605
3605
  throw new Error(`Unable to generate a random pipe name with ${randomLength} characters.`);
3606
3606
  }
3607
3607
  const randomSuffix = (0, crypto_1.randomBytes)(Math.floor(randomLength / 2)).toString("hex");
3608
- return path6.join(tmpDir, `lsp-${randomSuffix}.sock`);
3608
+ return path7.join(tmpDir, `lsp-${randomSuffix}.sock`);
3609
3609
  }
3610
3610
  function createClientPipeTransport(pipeName, encoding = "utf-8") {
3611
3611
  let connectResolve;
@@ -4509,10 +4509,10 @@ function mergeDefs(...defs) {
4509
4509
  function cloneDef(schema) {
4510
4510
  return mergeDefs(schema._zod.def);
4511
4511
  }
4512
- function getElementAtPath(obj, path6) {
4513
- if (!path6)
4512
+ function getElementAtPath(obj, path7) {
4513
+ if (!path7)
4514
4514
  return obj;
4515
- return path6.reduce((acc, key) => acc?.[key], obj);
4515
+ return path7.reduce((acc, key) => acc?.[key], obj);
4516
4516
  }
4517
4517
  function promiseAllObject(promisesObj) {
4518
4518
  const keys = Object.keys(promisesObj);
@@ -4921,11 +4921,11 @@ function explicitlyAborted(x, startIndex = 0) {
4921
4921
  }
4922
4922
  return false;
4923
4923
  }
4924
- function prefixIssues(path6, issues) {
4924
+ function prefixIssues(path7, issues) {
4925
4925
  return issues.map((iss) => {
4926
4926
  var _a3;
4927
4927
  (_a3 = iss).path ?? (_a3.path = []);
4928
- iss.path.unshift(path6);
4928
+ iss.path.unshift(path7);
4929
4929
  return iss;
4930
4930
  });
4931
4931
  }
@@ -5072,16 +5072,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
5072
5072
  }
5073
5073
  function formatError(error51, mapper = (issue2) => issue2.message) {
5074
5074
  const fieldErrors = { _errors: [] };
5075
- const processError = (error52, path6 = []) => {
5075
+ const processError = (error52, path7 = []) => {
5076
5076
  for (const issue2 of error52.issues) {
5077
5077
  if (issue2.code === "invalid_union" && issue2.errors.length) {
5078
- issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
5078
+ issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
5079
5079
  } else if (issue2.code === "invalid_key") {
5080
- processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
5080
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
5081
5081
  } else if (issue2.code === "invalid_element") {
5082
- processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
5082
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
5083
5083
  } else {
5084
- const fullpath = [...path6, ...issue2.path];
5084
+ const fullpath = [...path7, ...issue2.path];
5085
5085
  if (fullpath.length === 0) {
5086
5086
  fieldErrors._errors.push(mapper(issue2));
5087
5087
  } else {
@@ -5108,17 +5108,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
5108
5108
  }
5109
5109
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
5110
5110
  const result = { errors: [] };
5111
- const processError = (error52, path6 = []) => {
5111
+ const processError = (error52, path7 = []) => {
5112
5112
  var _a3, _b;
5113
5113
  for (const issue2 of error52.issues) {
5114
5114
  if (issue2.code === "invalid_union" && issue2.errors.length) {
5115
- issue2.errors.map((issues) => processError({ issues }, [...path6, ...issue2.path]));
5115
+ issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
5116
5116
  } else if (issue2.code === "invalid_key") {
5117
- processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
5117
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
5118
5118
  } else if (issue2.code === "invalid_element") {
5119
- processError({ issues: issue2.issues }, [...path6, ...issue2.path]);
5119
+ processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
5120
5120
  } else {
5121
- const fullpath = [...path6, ...issue2.path];
5121
+ const fullpath = [...path7, ...issue2.path];
5122
5122
  if (fullpath.length === 0) {
5123
5123
  result.errors.push(mapper(issue2));
5124
5124
  continue;
@@ -5150,8 +5150,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
5150
5150
  }
5151
5151
  function toDotPath(_path) {
5152
5152
  const segs = [];
5153
- const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5154
- for (const seg of path6) {
5153
+ const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
5154
+ for (const seg of path7) {
5155
5155
  if (typeof seg === "number")
5156
5156
  segs.push(`[${seg}]`);
5157
5157
  else if (typeof seg === "symbol")
@@ -17843,13 +17843,13 @@ function resolveRef(ref, ctx) {
17843
17843
  if (!ref.startsWith("#")) {
17844
17844
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
17845
17845
  }
17846
- const path6 = ref.slice(1).split("/").filter(Boolean);
17847
- if (path6.length === 0) {
17846
+ const path7 = ref.slice(1).split("/").filter(Boolean);
17847
+ if (path7.length === 0) {
17848
17848
  return ctx.rootSchema;
17849
17849
  }
17850
17850
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
17851
- if (path6[0] === defsKey) {
17852
- const key = path6[1];
17851
+ if (path7[0] === defsKey) {
17852
+ const key = path7[1];
17853
17853
  if (!key || !ctx.defs[key]) {
17854
17854
  throw new Error(`Reference not found: ${ref}`);
17855
17855
  }
@@ -23235,13 +23235,13 @@ function createTerminalCommandEvent(event, terminalId, cwd) {
23235
23235
  }
23236
23236
  };
23237
23237
  }
23238
- function createSearchTitle(query, path6) {
23239
- if (query && path6) {
23240
- return `Search for '${query}' in ${path6}`;
23238
+ function createSearchTitle(query, path7) {
23239
+ if (query && path7) {
23240
+ return `Search for '${query}' in ${path7}`;
23241
23241
  } else if (query) {
23242
23242
  return `Search for '${query}'`;
23243
- } else if (path6) {
23244
- return `Search in '${path6}'`;
23243
+ } else if (path7) {
23244
+ return `Search in '${path7}'`;
23245
23245
  }
23246
23246
  return "Search";
23247
23247
  }
@@ -23467,12 +23467,12 @@ function revertPatch(unifiedDiff) {
23467
23467
  if (!patch) return null;
23468
23468
  return reversePatch(patch);
23469
23469
  }
23470
- function createUpdateDiffContent(path6, oldText, newText) {
23470
+ function createUpdateDiffContent(path7, oldText, newText) {
23471
23471
  return {
23472
23472
  type: "diff",
23473
23473
  oldText,
23474
23474
  newText,
23475
- path: path6,
23475
+ path: path7,
23476
23476
  _meta: {
23477
23477
  kind: "update"
23478
23478
  }
@@ -23560,7 +23560,7 @@ function createAgentTextThoughtChunk(text, messageId, meta3) {
23560
23560
  var GOAL_EXTENSION_VERSION = 1;
23561
23561
  var GOAL_CONTROL_METHOD = "_session/goal";
23562
23562
  var LEGACY_GOAL_CONTROL_METHOD = "_codex/session/goal_control";
23563
- var GOAL_CONTROL_ACTIONS = ["pause", "resume", "clear"];
23563
+ var GOAL_CONTROL_ACTIONS = ["set", "pause", "resume", "clear"];
23564
23564
 
23565
23565
  // src/ThreadGoalSnapshot.ts
23566
23566
  function toGoalStatus(status) {
@@ -23597,10 +23597,57 @@ function sameThreadGoalSnapshot(left, right) {
23597
23597
  }
23598
23598
 
23599
23599
  // src/CodexEventHandler.ts
23600
+ import { randomUUID } from "node:crypto";
23601
+
23602
+ // src/AirExtension.ts
23603
+ var JETBRAINS_META_KEY = "jetbrains";
23604
+ var AIR_META_KEY = "air";
23605
+ var AIR_EXTENSION_VERSION_KEY = "version";
23606
+ var AIR_EXTENSION_CAPABILITIES_KEY = "capabilities";
23607
+ var AIR_SESSION_FAILURE_KEY = "sessionFailure";
23608
+ var AIR_EXTENSION_VERSION = 1;
23609
+
23610
+ // src/CodexEventHandler.ts
23611
+ var SESSION_FAILURE_PRESENTATION = {
23612
+ transport_lost: { message: "Connection to Codex was lost.", retryable: true, actions: ["reconnect", "retry"] },
23613
+ auth_required: { message: "Sign in to continue using Codex.", retryable: false, actions: ["login"] },
23614
+ rate_limited: { message: "The Codex rate limit was reached.", retryable: true, actions: ["retry"] },
23615
+ quota_exhausted: { message: "The Codex usage quota is exhausted.", retryable: false, actions: ["new_session"] },
23616
+ overloaded: { message: "Codex is temporarily overloaded.", retryable: true, actions: ["retry"] },
23617
+ context_exhausted: { message: "This conversation has reached its context limit.", retryable: false, actions: ["new_turn"] },
23618
+ budget_exhausted: { message: "This session has reached its usage budget.", retryable: false, actions: ["new_session"] },
23619
+ policy_denied: { message: "The request was blocked by provider policy.", retryable: false, actions: [] },
23620
+ bad_request: { message: "Codex could not process this request.", retryable: false, actions: ["new_turn"] },
23621
+ provider_error: { message: "The model provider reported an error.", retryable: true, actions: ["retry"] },
23622
+ internal_error: { message: "Codex encountered an internal error.", retryable: true, actions: ["retry"] }
23623
+ };
23624
+ var STRING_CODEX_ERROR_CATEGORIES = {
23625
+ contextWindowExceeded: "context_exhausted",
23626
+ sessionBudgetExceeded: "budget_exhausted",
23627
+ usageLimitExceeded: "quota_exhausted",
23628
+ serverOverloaded: "overloaded",
23629
+ cyberPolicy: "policy_denied",
23630
+ internalServerError: "internal_error",
23631
+ unauthorized: "auth_required",
23632
+ badRequest: "bad_request",
23633
+ threadRollbackFailed: "provider_error",
23634
+ sandboxError: "provider_error",
23635
+ other: "provider_error"
23636
+ };
23637
+ var STRUCTURED_CODEX_ERROR_CATEGORIES = {
23638
+ httpConnectionFailed: "transport_lost",
23639
+ responseStreamConnectionFailed: "transport_lost",
23640
+ responseStreamDisconnected: "transport_lost",
23641
+ responseTooManyFailedAttempts: "transport_lost",
23642
+ activeTurnNotSteerable: "provider_error"
23643
+ };
23600
23644
  var CodexEventHandler = class _CodexEventHandler {
23601
23645
  static PLAN_UPDATE_INTERVAL_MS = 150;
23602
23646
  sessionState;
23603
23647
  supportsPlanUpdates;
23648
+ supportsTypedSessionFailures;
23649
+ sessionFailureEpoch;
23650
+ pendingErrors = [];
23604
23651
  failure = null;
23605
23652
  completedPlan = null;
23606
23653
  activeFuzzyFileSearchSessions = /* @__PURE__ */ new Set();
@@ -23619,20 +23666,108 @@ var CodexEventHandler = class _CodexEventHandler {
23619
23666
  terminalCommandOutputIds = /* @__PURE__ */ new Set();
23620
23667
  agentMessagePhases = /* @__PURE__ */ new Map();
23621
23668
  activeSubAgentActivities = /* @__PURE__ */ new Set();
23622
- constructor(connection, sessionState, supportsPlanUpdates = false) {
23669
+ constructor(connection, sessionState, supportsPlanUpdates = false, supportsTypedSessionFailures = false, sessionFailureEpoch = randomUUID()) {
23623
23670
  this.sessionState = sessionState;
23624
23671
  this.supportsPlanUpdates = supportsPlanUpdates;
23672
+ this.supportsTypedSessionFailures = supportsTypedSessionFailures;
23673
+ this.sessionFailureEpoch = sessionFailureEpoch;
23625
23674
  this.session = new ACPSessionConnection(connection, sessionState.sessionId);
23626
23675
  }
23627
23676
  getFailure() {
23628
23677
  return this.failure;
23629
23678
  }
23679
+ getTerminalSessionFailureMeta(turnId, allowUnattributed = false) {
23680
+ const failure = this.sessionState.sessionFailure;
23681
+ if (!this.supportsTypedSessionFailures || failure?.phase !== "active" || (failure.turnId === void 0 ? !allowUnattributed : turnId === null || failure.turnId !== turnId)) {
23682
+ return null;
23683
+ }
23684
+ return this.createSessionFailureMeta(failure);
23685
+ }
23686
+ recordSyntheticTerminalFailure(category, turnId) {
23687
+ this.recordSessionFailure(category, turnId ?? void 0);
23688
+ }
23689
+ /**
23690
+ * Handles notifications after the prompt-local handler has been disposed. The app-server subscription
23691
+ * remains installed until the ACP session closes, so terminal errors need a durable session-level path
23692
+ * instead of entering a turn buffer that will never be flushed.
23693
+ */
23694
+ async handleSessionScopedNotification(notification) {
23695
+ if (notification.method !== "error") {
23696
+ await this.handleNotification(notification);
23697
+ return;
23698
+ }
23699
+ if (!this.supportsTypedSessionFailures) {
23700
+ await this.handleNotification(notification);
23701
+ return;
23702
+ }
23703
+ if (notification.params.willRetry) {
23704
+ await this.session.update(this.createSafeErrorDiagnostic(notification.params));
23705
+ return;
23706
+ }
23707
+ const failure = this.recordSessionFailure(
23708
+ this.sessionFailureCategory(notification.params.error.codexErrorInfo),
23709
+ void 0
23710
+ );
23711
+ await this.session.update(this.createSessionFailureUpdate(failure));
23712
+ }
23713
+ async flushPendingErrors() {
23714
+ if (this.sessionState.currentTurnId === null || this.pendingErrors.length === 0) {
23715
+ return;
23716
+ }
23717
+ const errors = this.pendingErrors.splice(0);
23718
+ for (const error51 of errors) {
23719
+ const update = await this.createErrorEvent(error51);
23720
+ if (update) {
23721
+ await this.session.update(update);
23722
+ }
23723
+ }
23724
+ }
23725
+ async flushPendingErrorsAsSessionScoped() {
23726
+ if (!this.supportsTypedSessionFailures || this.pendingErrors.length === 0) {
23727
+ return;
23728
+ }
23729
+ const errors = this.pendingErrors.splice(0);
23730
+ for (const error51 of errors) {
23731
+ await this.handleSessionScopedNotification({ method: "error", params: error51 });
23732
+ }
23733
+ }
23734
+ async clearSessionFailure() {
23735
+ const active = this.sessionState.sessionFailure;
23736
+ if (!this.supportsTypedSessionFailures || active?.phase !== "active") {
23737
+ return;
23738
+ }
23739
+ const cleared = {
23740
+ ...active,
23741
+ revision: active.revision + 1,
23742
+ phase: "cleared"
23743
+ };
23744
+ await this.session.update(this.createSessionFailureUpdate(cleared));
23745
+ this.sessionState.sessionFailure = cleared;
23746
+ }
23747
+ async handleFailedTurn(turn) {
23748
+ const activeFailure = this.sessionState.sessionFailure;
23749
+ if (!this.supportsTypedSessionFailures || turn.status !== "failed" || this.failure !== null || activeFailure?.phase === "active" && activeFailure.turnId === turn.id) {
23750
+ return;
23751
+ }
23752
+ const error51 = turn.error ?? {
23753
+ message: "Turn failed",
23754
+ codexErrorInfo: null,
23755
+ additionalDetails: null
23756
+ };
23757
+ this.recordTypedSessionFailure({
23758
+ threadId: this.sessionState.sessionId,
23759
+ turnId: turn.id,
23760
+ willRetry: false,
23761
+ error: error51
23762
+ });
23763
+ }
23630
23764
  takeCompletedPlan() {
23631
23765
  const plan = this.completedPlan;
23632
23766
  this.completedPlan = null;
23633
23767
  return plan;
23634
23768
  }
23635
23769
  async handleNotification(notification) {
23770
+ await this.flushPendingErrors();
23636
23771
  const updateEvent = await this.createUpdateEvent(notification);
23637
23772
  if (updateEvent) {
23638
23773
  await this.session.update(updateEvent);
@@ -23653,6 +23788,14 @@ var CodexEventHandler = class _CodexEventHandler {
23653
23788
  async dispose() {
23654
23789
  if (this.disposed) return;
23655
23790
  await this.flushPendingPlanUpdates();
23791
+ if (this.pendingErrors.length > 0) {
23792
+ logger.log("Discarding app-server errors that arrived before a turn started", {
23793
+ sessionId: this.sessionState.sessionId,
23794
+ count: this.pendingErrors.length,
23795
+ turnIds: this.pendingErrors.map((error51) => error51.turnId)
23796
+ });
23797
+ this.pendingErrors.splice(0);
23798
+ }
23656
23799
  this.disposed = true;
23657
23800
  this.cancelPlanUpdateTimer();
23658
23801
  this.pendingPlanItemIds.clear();
@@ -23675,6 +23818,7 @@ var CodexEventHandler = class _CodexEventHandler {
23675
23818
  return await this.createErrorEvent(notification.params);
23676
23819
  case "turn/started":
23677
23820
  this.sessionState.currentTurnId = notification.params.turn.id;
23821
+ await this.flushPendingErrors();
23678
23822
  return null;
23679
23823
  case "turn/completed":
23680
23824
  await this.flushPendingPlanUpdates();
@@ -24174,7 +24318,27 @@ ${event.stdin}
24174
24318
  }
24175
24319
  async createErrorEvent(params) {
24176
24320
  const error51 = params.error.codexErrorInfo;
24321
+ if (this.sessionState.currentTurnId === null) {
24322
+ this.pendingErrors.push(params);
24323
+ logger.log("Buffered app-server error until the active turn is known", {
24324
+ sessionId: this.sessionState.sessionId,
24325
+ turnId: params.turnId,
24326
+ willRetry: params.willRetry
24327
+ });
24328
+ return null;
24329
+ }
24330
+ if (params.turnId !== this.sessionState.currentTurnId) {
24331
+ if (this.supportsTypedSessionFailures) {
24332
+ return this.createSafeErrorDiagnostic(params);
24333
+ }
24334
+ return this.createCodexSessionInfoUpdate({
24335
+ error: { ...params.error, turnId: params.turnId, willRetry: params.willRetry }
24336
+ });
24337
+ }
24177
24338
  if (params.willRetry) {
24339
+ if (this.supportsTypedSessionFailures) {
24340
+ return this.createSafeErrorDiagnostic(params);
24341
+ }
24178
24342
  return this.createCodexSessionInfoUpdate({
24179
24343
  error: {
24180
24344
  ...params.error,
@@ -24182,7 +24346,12 @@ ${event.stdin}
24182
24346
  willRetry: true
24183
24347
  }
24184
24348
  });
24185
- } else if (error51 === "usageLimitExceeded") {
24349
+ }
24350
+ if (this.supportsTypedSessionFailures) {
24351
+ this.recordTypedSessionFailure(params);
24352
+ return null;
24353
+ }
24354
+ if (error51 === "usageLimitExceeded") {
24186
24355
  this.failure = RequestError.internalError(
24187
24356
  this.createTurnErrorData(params.error)
24188
24357
  );
@@ -24193,22 +24362,78 @@ ${event.stdin}
24193
24362
 
24194
24363
  `);
24195
24364
  }
24365
+ createSafeErrorDiagnostic(params) {
24366
+ const category = this.sessionFailureCategory(params.error.codexErrorInfo);
24367
+ return this.createCodexSessionInfoUpdate({
24368
+ error: {
24369
+ category,
24370
+ message: SESSION_FAILURE_PRESENTATION[category].message,
24371
+ turnId: params.turnId,
24372
+ willRetry: params.willRetry
24373
+ }
24374
+ });
24375
+ }
24376
+ recordTypedSessionFailure(params) {
24377
+ const category = this.sessionFailureCategory(params.error.codexErrorInfo);
24378
+ this.recordSessionFailure(category, params.turnId);
24379
+ }
24380
+ recordSessionFailure(category, turnId) {
24381
+ const presentation = SESSION_FAILURE_PRESENTATION[category];
24382
+ const previous = this.sessionState.sessionFailure;
24383
+ const id = previous?.phase === "active" ? previous.id : turnId === void 0 ? `${this.sessionState.sessionId}:error:${this.sessionFailureEpoch}` : `${turnId}:error`;
24384
+ const failure = {
24385
+ id,
24386
+ revision: previous?.id === id ? previous.revision + 1 : 1,
24387
+ phase: "active",
24388
+ category,
24389
+ source: "codex",
24390
+ safeMessage: presentation.message,
24391
+ retryable: presentation.retryable,
24392
+ actions: presentation.actions,
24393
+ ...turnId === void 0 ? {} : { turnId }
24394
+ };
24395
+ this.sessionState.sessionFailure = failure;
24396
+ return failure;
24397
+ }
24398
+ createSessionFailureMeta(failure) {
24399
+ return {
24400
+ [JETBRAINS_META_KEY]: {
24401
+ [AIR_META_KEY]: {
24402
+ [AIR_EXTENSION_VERSION_KEY]: AIR_EXTENSION_VERSION,
24403
+ [AIR_SESSION_FAILURE_KEY]: failure
24404
+ }
24405
+ }
24406
+ };
24407
+ }
24408
+ createSessionFailureUpdate(failure) {
24409
+ return {
24410
+ sessionUpdate: "session_info_update",
24411
+ _meta: this.createSessionFailureMeta(failure)
24412
+ };
24413
+ }
24414
+ sessionFailureCategory(error51) {
24415
+ if (this.isAuthenticationRequiredError(error51)) return "auth_required";
24416
+ if (this.getHttpStatusCode(error51) === 429) return "rate_limited";
24417
+ if (typeof error51 === "string") {
24418
+ return STRING_CODEX_ERROR_CATEGORIES[error51] ?? "provider_error";
24419
+ }
24420
+ if (error51 !== null) {
24421
+ for (const kind of Object.keys(STRUCTURED_CODEX_ERROR_CATEGORIES)) {
24422
+ if (kind in error51) {
24423
+ return STRUCTURED_CODEX_ERROR_CATEGORIES[kind] ?? "provider_error";
24424
+ }
24425
+ }
24426
+ }
24427
+ return "provider_error";
24428
+ }
24196
24429
  isAuthenticationRequiredError(error51) {
24197
24430
  return error51 === "unauthorized" || this.getHttpStatusCode(error51) === 401;
24198
24431
  }
24199
24432
  getHttpStatusCode(error51) {
24200
- if (error51 !== null && typeof error51 === "object") {
24201
- if ("httpConnectionFailed" in error51) {
24202
- return error51.httpConnectionFailed.httpStatusCode;
24203
- } else if ("responseStreamConnectionFailed" in error51) {
24204
- return error51.responseStreamConnectionFailed.httpStatusCode;
24205
- } else if ("responseStreamDisconnected" in error51) {
24206
- return error51.responseStreamDisconnected.httpStatusCode;
24207
- } else if ("responseTooManyFailedAttempts" in error51) {
24208
- return error51.responseTooManyFailedAttempts.httpStatusCode;
24209
- }
24210
- }
24211
- return null;
24433
+ if (error51 === null || typeof error51 !== "object") return null;
24434
+ const details = Object.values(error51)[0];
24435
+ if (details === null || typeof details !== "object" || !("httpStatusCode" in details)) return null;
24436
+ return typeof details.httpStatusCode === "number" ? details.httpStatusCode : null;
24212
24437
  }
24213
24438
  createTurnErrorData(error51) {
24214
24439
  const data = {
@@ -24637,11 +24862,11 @@ var CodexApprovalHandler = class {
24637
24862
  });
24638
24863
  }
24639
24864
  const fileSystem = permissions.fileSystem;
24640
- for (const path6 of fileSystem?.read ?? []) {
24641
- changes.push(this.fileSystemGrantChange(path6, "read", lifetime, suffix));
24865
+ for (const path7 of fileSystem?.read ?? []) {
24866
+ changes.push(this.fileSystemGrantChange(path7, "read", lifetime, suffix));
24642
24867
  }
24643
- for (const path6 of fileSystem?.write ?? []) {
24644
- changes.push(this.fileSystemGrantChange(path6, "write", lifetime, suffix));
24868
+ for (const path7 of fileSystem?.write ?? []) {
24869
+ changes.push(this.fileSystemGrantChange(path7, "write", lifetime, suffix));
24645
24870
  }
24646
24871
  for (const entry of fileSystem?.entries ?? []) {
24647
24872
  const matcher = (() => {
@@ -24670,16 +24895,16 @@ var CodexApprovalHandler = class {
24670
24895
  }
24671
24896
  return changes.length > 0 ? { version: 1, changes } : void 0;
24672
24897
  }
24673
- fileSystemGrantChange(path6, access, lifetime, suffix) {
24898
+ fileSystemGrantChange(path7, access, lifetime, suffix) {
24674
24899
  return {
24675
24900
  type: "grant",
24676
24901
  operation: "grant",
24677
- description: `Allow ${access} access to ${path6}${suffix}`,
24902
+ description: `Allow ${access} access to ${path7}${suffix}`,
24678
24903
  lifetime,
24679
24904
  targets: [{
24680
24905
  type: "filesystem",
24681
24906
  access: [access],
24682
- matcher: { type: "exact_path", path: path6 }
24907
+ matcher: { type: "exact_path", path: path7 }
24683
24908
  }]
24684
24909
  };
24685
24910
  }
@@ -25589,15 +25814,15 @@ var wslDefaultBrowser = async () => {
25589
25814
  const { stdout } = await executePowerShell(command, { powerShellPath: psPath });
25590
25815
  return stdout.trim();
25591
25816
  };
25592
- var convertWslPathToWindows = async (path6) => {
25593
- if (/^[a-z]+:\/\//i.test(path6)) {
25594
- return path6;
25817
+ var convertWslPathToWindows = async (path7) => {
25818
+ if (/^[a-z]+:\/\//i.test(path7)) {
25819
+ return path7;
25595
25820
  }
25596
25821
  try {
25597
- const { stdout } = await execFile2("wslpath", ["-aw", path6], { encoding: "utf8" });
25822
+ const { stdout } = await execFile2("wslpath", ["-aw", path7], { encoding: "utf8" });
25598
25823
  return stdout.trim();
25599
25824
  } catch {
25600
- return path6;
25825
+ return path7;
25601
25826
  }
25602
25827
  };
25603
25828
 
@@ -26147,7 +26372,7 @@ var AgentMode = class _AgentMode {
26147
26372
  };
26148
26373
 
26149
26374
  // src/CodexAcpClient.ts
26150
- import path4 from "node:path";
26375
+ import path5 from "node:path";
26151
26376
 
26152
26377
  // src/McpServerName.ts
26153
26378
  var MCP_SERVER_NAME_WHITESPACE = /\p{White_Space}/gu;
@@ -26161,7 +26386,7 @@ var package_default = {
26161
26386
  publishConfig: {
26162
26387
  access: "public"
26163
26388
  },
26164
- version: "1.1.13",
26389
+ version: "1.2.0",
26165
26390
  description: "",
26166
26391
  main: "dist/index.js",
26167
26392
  bin: {
@@ -26194,6 +26419,7 @@ var package_default = {
26194
26419
  "example:steering": "node --import tsx examples/steering.ts",
26195
26420
  "example:steering:multistep": "node --import tsx examples/steering.ts",
26196
26421
  "generate-types": "./node_modules/.bin/codex app-server generate-ts --out src/app-server",
26422
+ "release:preflight": "bash scripts/release-preflight.sh",
26197
26423
  test: "vitest run",
26198
26424
  "test:e2e": "npm run build && RUN_E2E_TESTS=true vitest run src/__tests__/CodexACPAgent/e2e",
26199
26425
  "test:watch": "vitest",
@@ -26222,7 +26448,7 @@ var package_default = {
26222
26448
  },
26223
26449
  dependencies: {
26224
26450
  "@agentclientprotocol/sdk": "^1.3.0",
26225
- "@openai/codex": "^0.146.1",
26451
+ "@openai/codex": "^0.147.0",
26226
26452
  diff: "^9.0.0",
26227
26453
  open: "^11.0.0",
26228
26454
  "vscode-jsonrpc": "^9.0.1",
@@ -26265,6 +26491,54 @@ function createCodexCollaborationMode(mode, currentModelId) {
26265
26491
  };
26266
26492
  }
26267
26493
 
26494
+ // src/PathUtils.ts
26495
+ import path4 from "node:path";
26496
+ function isAbsolutePathLike(value) {
26497
+ const trimmed = value.trim();
26498
+ return path4.isAbsolute(trimmed) || isWindowsAbsolutePath(trimmed);
26499
+ }
26500
+ function arePathsEqual(left, right) {
26501
+ return normalizePathForComparison(left) === normalizePathForComparison(right);
26502
+ }
26503
+ function arePathBasenamesEqual(left, right) {
26504
+ const leftBase = path4.posix.basename(normalizePathForComparison(left));
26505
+ const rightBase = path4.posix.basename(normalizePathForComparison(right));
26506
+ if (shouldComparePathCaseInsensitive(left) || shouldComparePathCaseInsensitive(right)) {
26507
+ return leftBase.toLowerCase() === rightBase.toLowerCase();
26508
+ }
26509
+ return leftBase === rightBase;
26510
+ }
26511
+ function normalizePathForComparison(value) {
26512
+ const trimmed = value.trim();
26513
+ if (trimmed.length === 0) {
26514
+ return "";
26515
+ }
26516
+ if (isWindowsAbsolutePath(trimmed)) {
26517
+ const normalized2 = path4.win32.normalize(trimmed).replace(/\\/g, "/");
26518
+ return trimTrailingPathSeparators(normalized2).toLowerCase();
26519
+ }
26520
+ const pathForComparison = path4.isAbsolute(trimmed) ? trimmed : trimmed.replace(/\\/g, "/");
26521
+ const normalized = path4.posix.normalize(pathForComparison);
26522
+ return trimTrailingPathSeparators(normalized);
26523
+ }
26524
+ function isWindowsAbsolutePath(value) {
26525
+ const portableValue = value.replace(/\\/g, "/");
26526
+ return /^[A-Za-z]:\//.test(portableValue) || /^\/\/[^/]+\/[^/]+/.test(portableValue);
26527
+ }
26528
+ function shouldComparePathCaseInsensitive(value) {
26529
+ return isWindowsAbsolutePath(value) || /^[A-Za-z]:/.test(value) || value.includes("\\");
26530
+ }
26531
+ function trimTrailingPathSeparators(value) {
26532
+ let trimmed = value;
26533
+ while (trimmed.endsWith("/") && !isRootPath(trimmed)) {
26534
+ trimmed = trimmed.slice(0, -1);
26535
+ }
26536
+ return trimmed;
26537
+ }
26538
+ function isRootPath(value) {
26539
+ return value === "/" || /^[A-Za-z]:\/$/.test(value) || /^\/\/[^/]+\/[^/]+\/$/.test(value);
26540
+ }
26541
+
26268
26542
  // src/CodexAcpClient.ts
26269
26543
  var CUSTOM_GATEWAY_PROVIDER_ID = "custom-gateway";
26270
26544
  var SUPPORTED_GATEWAY_PROTOCOLS = {
@@ -26624,12 +26898,16 @@ var CodexAcpClient = class {
26624
26898
  const response = await this.codexClient.threadGoalGet({ threadId: sessionId });
26625
26899
  return response?.goal ?? null;
26626
26900
  }
26627
- async setGoal(sessionId, objective, onTurnStarted) {
26628
- return await this.codexClient.runGoalSet({
26901
+ async setGoal(sessionId, objective, onTurnStarted, onGoalSet) {
26902
+ const params = {
26629
26903
  threadId: sessionId,
26630
26904
  objective,
26631
26905
  status: "active"
26632
- }, onTurnStarted);
26906
+ };
26907
+ if (onGoalSet === void 0) {
26908
+ return await this.codexClient.runGoalSet(params, onTurnStarted);
26909
+ }
26910
+ return await this.codexClient.runGoalSet(params, onTurnStarted, void 0, onGoalSet);
26633
26911
  }
26634
26912
  async setGoalStatus(sessionId, status) {
26635
26913
  let updatedGoal = null;
@@ -26644,11 +26922,15 @@ var CodexAcpClient = class {
26644
26922
  }
26645
26923
  return updatedGoal;
26646
26924
  }
26647
- async resumeGoal(sessionId, onTurnStarted) {
26648
- return await this.codexClient.runGoalSet({
26925
+ async resumeGoal(sessionId, onTurnStarted, onGoalSet) {
26926
+ const params = {
26649
26927
  threadId: sessionId,
26650
26928
  status: "active"
26651
- }, onTurnStarted);
26929
+ };
26930
+ if (onGoalSet === void 0) {
26931
+ return await this.codexClient.runGoalSet(params, onTurnStarted);
26932
+ }
26933
+ return await this.codexClient.runGoalSet(params, onTurnStarted, void 0, onGoalSet);
26652
26934
  }
26653
26935
  async clearGoal(sessionId) {
26654
26936
  await this.codexClient.runGoalClear({ threadId: sessionId });
@@ -26711,7 +26993,7 @@ var CodexAcpClient = class {
26711
26993
  if (!cwd) {
26712
26994
  return;
26713
26995
  }
26714
- const skillExtraRoots = additionalRoots.map((root) => path4.join(root, ".agents", "skills"));
26996
+ const skillExtraRoots = additionalRoots.map((root) => path5.join(root, ".agents", "skills"));
26715
26997
  if (!arraysEqual(this.skillExtraRoots, skillExtraRoots)) {
26716
26998
  await this.codexClient.skillsExtraRootsSet({ extraRoots: skillExtraRoots });
26717
26999
  this.skillExtraRoots = skillExtraRoots;
@@ -26902,11 +27184,10 @@ var CodexAcpClient = class {
26902
27184
  const requestedCwd = request.cwd?.trim() ?? null;
26903
27185
  const filterByCwd = (thread) => {
26904
27186
  if (!requestedCwd) return true;
26905
- if (path4.isAbsolute(requestedCwd)) {
26906
- return thread.cwd === requestedCwd;
27187
+ if (isAbsolutePathLike(requestedCwd)) {
27188
+ return arePathsEqual(thread.cwd, requestedCwd);
26907
27189
  }
26908
- const requestedBase = path4.basename(requestedCwd);
26909
- return path4.basename(thread.cwd) === requestedBase;
27190
+ return arePathBasenamesEqual(thread.cwd, requestedCwd);
26910
27191
  };
26911
27192
  const preferredProvider = this.getModelProvider();
26912
27193
  const modelProviders = preferredProvider ? [preferredProvider] : [];
@@ -26928,7 +27209,7 @@ var CodexAcpClient = class {
26928
27209
  let sessions = listResponse.data.map(mapThreadToSession);
26929
27210
  if (requestedCwd) {
26930
27211
  const filtered = listResponse.data.filter(filterByCwd).map(mapThreadToSession);
26931
- if (filtered.length > 0 || path4.isAbsolute(requestedCwd)) {
27212
+ if (filtered.length > 0 || isAbsolutePathLike(requestedCwd)) {
26932
27213
  sessions = filtered;
26933
27214
  } else {
26934
27215
  logger.log("Ignoring non-absolute cwd filter for session/list", { cwd: requestedCwd });
@@ -27043,8 +27324,8 @@ function formatUriAsLink(name, uri) {
27043
27324
  return `[@${name}](${uri})`;
27044
27325
  }
27045
27326
  if (uri.startsWith("file://")) {
27046
- const path6 = uri.replace("file://", "");
27047
- const fileName = path6.split("/").pop() ?? path6;
27327
+ const path7 = uri.replace("file://", "");
27328
+ const fileName = path7.split("/").pop() ?? path7;
27048
27329
  return `[@${fileName}](${uri})`;
27049
27330
  }
27050
27331
  return uri;
@@ -27074,7 +27355,7 @@ function readAdditionalDirectories(cwd, additionalDirectories, meta3) {
27074
27355
  if (directory.length === 0) {
27075
27356
  throw RequestError.invalidParams(void 0, "additionalDirectories entries must not be empty");
27076
27357
  }
27077
- if (!path4.isAbsolute(directory)) {
27358
+ if (!path5.isAbsolute(directory)) {
27078
27359
  throw RequestError.invalidParams(void 0, "additionalDirectories entries must be absolute paths");
27079
27360
  }
27080
27361
  if (!seen.has(directory)) {
@@ -27189,6 +27470,10 @@ function createReasoningEffortConfigOption(supportedReasoningEfforts, currentEff
27189
27470
  }
27190
27471
 
27191
27472
  // src/CodexCommands.ts
27473
+ var GOAL_CONTINUATION_PROMPT = [{
27474
+ type: "text",
27475
+ text: "Continue working toward the active goal."
27476
+ }];
27192
27477
  var CodexCommands = class {
27193
27478
  connection;
27194
27479
  codexAcpClient;
@@ -27477,7 +27762,7 @@ var CodexCommands = class {
27477
27762
  }
27478
27763
  createGoalCommandResult(turnCompleted) {
27479
27764
  if (turnCompleted === null) {
27480
- return { handled: true };
27765
+ return { handled: false, prompt: GOAL_CONTINUATION_PROMPT };
27481
27766
  }
27482
27767
  return {
27483
27768
  handled: true,
@@ -27673,7 +27958,7 @@ var SteeringQueue = class {
27673
27958
 
27674
27959
  // src/ResponseItemHistoryFallback.ts
27675
27960
  import { readFile as readFile2 } from "node:fs/promises";
27676
- import path5 from "node:path";
27961
+ import path6 from "node:path";
27677
27962
  function historyFallbackUpdateKey(update) {
27678
27963
  switch (update.sessionUpdate) {
27679
27964
  case "user_message_chunk":
@@ -27913,8 +28198,8 @@ function imageBlocks(images) {
27913
28198
  return [{ type: "text", text: `[@image](${image})` }];
27914
28199
  }
27915
28200
  const record2 = asRecord(image);
27916
- const path6 = record2 ? stringValue(record2["path"]) ?? stringValue(record2["url"]) : null;
27917
- return path6 ? [{ type: "text", text: `[@image](${path6})` }] : [];
28201
+ const path7 = record2 ? stringValue(record2["path"]) ?? stringValue(record2["url"]) : null;
28202
+ return path7 ? [{ type: "text", text: `[@image](${path7})` }] : [];
27918
28203
  });
27919
28204
  }
27920
28205
  function contentBlocksFromResponseContent(content) {
@@ -28546,10 +28831,10 @@ function baseCommandName(commandName) {
28546
28831
  return commandName.split(/[\\/]/).pop() ?? commandName;
28547
28832
  }
28548
28833
  function absolutizePath(cwd, targetPath) {
28549
- if (path5.isAbsolute(targetPath) || cwd.length === 0) {
28834
+ if (path6.isAbsolute(targetPath) || cwd.length === 0) {
28550
28835
  return targetPath;
28551
28836
  }
28552
- return path5.join(cwd, targetPath);
28837
+ return path6.join(cwd, targetPath);
28553
28838
  }
28554
28839
  function withTerminalContent(event, terminalId, cwd) {
28555
28840
  const { rawInput, ...eventWithoutRawInput } = event;
@@ -28705,8 +28990,17 @@ function clientSupportsPlanUpdates(clientCapabilities) {
28705
28990
  }
28706
28991
 
28707
28992
  // src/CodexAcpServer.ts
28993
+ import { randomUUID as randomUUID2 } from "node:crypto";
28708
28994
  var IMPLEMENT_PLAN_OPTION_ID = "implement_plan";
28709
28995
  var REVISE_PLAN_OPTION_ID = "revise_plan";
28996
+ var CODEX_PROCESS_EXITED_ERROR_CODE = 1001;
28997
+ function clientSupportsTypedSessionFailures(capabilities) {
28998
+ const jetbrains = capabilities?._meta?.[JETBRAINS_META_KEY];
28999
+ const air = jetbrains?.[AIR_META_KEY];
29000
+ const version2 = air?.[AIR_EXTENSION_VERSION_KEY];
29001
+ const supported = air?.[AIR_EXTENSION_CAPABILITIES_KEY];
29002
+ return typeof version2 === "number" && Number.isInteger(version2) && version2 >= AIR_EXTENSION_VERSION && Array.isArray(supported) && supported.includes(AIR_SESSION_FAILURE_KEY);
29003
+ }
28710
29004
  var CodexAcpServer = class _CodexAcpServer {
28711
29005
  static MODEL_NAME_TOKEN_OVERRIDES = {
28712
29006
  gpt: "GPT",
@@ -28718,6 +29012,7 @@ var CodexAcpServer = class _CodexAcpServer {
28718
29012
  defaultAuthRequest;
28719
29013
  getExitCode;
28720
29014
  getRecentStderr;
29015
+ sessionFailureEpoch;
28721
29016
  availableCommands;
28722
29017
  clientInfo;
28723
29018
  clientCapabilities;
@@ -28731,6 +29026,7 @@ var CodexAcpServer = class _CodexAcpServer {
28731
29026
  closingSessions;
28732
29027
  sessionGenerations;
28733
29028
  sessionOpenGenerations;
29029
+ goalControlGenerations;
28734
29030
  constructor(connection, codexAcpClient, defaultAuthRequest, getExitCode, getRecentStderr) {
28735
29031
  this.sessions = /* @__PURE__ */ new Map();
28736
29032
  this.pendingMcpStartupSessions = /* @__PURE__ */ new Map();
@@ -28740,11 +29036,13 @@ var CodexAcpServer = class _CodexAcpServer {
28740
29036
  this.closingSessions = /* @__PURE__ */ new Map();
28741
29037
  this.sessionGenerations = /* @__PURE__ */ new Map();
28742
29038
  this.sessionOpenGenerations = /* @__PURE__ */ new Map();
29039
+ this.goalControlGenerations = /* @__PURE__ */ new Map();
28743
29040
  this.connection = connection;
28744
29041
  this.codexAcpClient = codexAcpClient;
28745
29042
  this.defaultAuthRequest = defaultAuthRequest ?? null;
28746
29043
  this.getExitCode = getExitCode ?? (() => null);
28747
29044
  this.getRecentStderr = getRecentStderr ?? (() => "");
29045
+ this.sessionFailureEpoch = randomUUID2();
28748
29046
  this.clientInfo = null;
28749
29047
  this.clientCapabilities = null;
28750
29048
  this.terminalOutputMode = "terminal_output_delta";
@@ -28802,6 +29100,12 @@ var CodexAcpServer = class _CodexAcpServer {
28802
29100
  version: GOAL_EXTENSION_VERSION,
28803
29101
  controlMethod: GOAL_CONTROL_METHOD,
28804
29102
  actions: [...GOAL_CONTROL_ACTIONS]
29103
+ },
29104
+ [JETBRAINS_META_KEY]: {
29105
+ [AIR_META_KEY]: {
29106
+ [AIR_EXTENSION_VERSION_KEY]: AIR_EXTENSION_VERSION,
29107
+ [AIR_EXTENSION_CAPABILITIES_KEY]: [AIR_SESSION_FAILURE_KEY]
29108
+ }
28805
29109
  }
28806
29110
  }
28807
29111
  };
@@ -28829,12 +29133,51 @@ var CodexAcpServer = class _CodexAcpServer {
28829
29133
  throw RequestError.invalidParams(void 0, `Unknown session: ${methodRequest.params.sessionId}`);
28830
29134
  }
28831
29135
  const sessionGeneration = this.getSessionGeneration(sessionState.sessionId);
28832
- if (methodRequest.params.action === "pause" || methodRequest.params.action === "resume") {
28833
- const status = methodRequest.params.action === "pause" ? "paused" : "active";
28834
- const goal = await this.runWithProcessCheck(() => this.codexAcpClient.setGoalStatus(sessionState.sessionId, status));
29136
+ const goalControlGeneration = this.bumpGoalControlGeneration(sessionState.sessionId);
29137
+ if (methodRequest.params.action === "set") {
29138
+ const objective = methodRequest.params.objective;
29139
+ let updatedGoal = null;
29140
+ const turnCompleted = await this.runWithProcessCheck(() => this.codexAcpClient.setGoal(
29141
+ sessionState.sessionId,
29142
+ objective,
29143
+ void 0,
29144
+ (goal) => {
29145
+ updatedGoal = goal;
29146
+ }
29147
+ ));
29148
+ if (turnCompleted === null && updatedGoal !== null) {
29149
+ await this.startGoalContinuationIfCurrent(
29150
+ sessionState,
29151
+ sessionGeneration,
29152
+ goalControlGeneration,
29153
+ updatedGoal
29154
+ );
29155
+ }
29156
+ } else if (methodRequest.params.action === "pause") {
29157
+ const goal = await this.runWithProcessCheck(() => this.codexAcpClient.setGoalStatus(sessionState.sessionId, "paused"));
28835
29158
  if (this.goalPublishIsCurrent(sessionState, sessionGeneration)) {
28836
29159
  await this.publishGoalSnapshot(sessionState, toThreadGoalSnapshot(goal), false);
28837
29160
  }
29161
+ } else if (methodRequest.params.action === "resume") {
29162
+ let updatedGoal = null;
29163
+ const turnCompleted = await this.runWithProcessCheck(() => this.codexAcpClient.resumeGoal(
29164
+ sessionState.sessionId,
29165
+ void 0,
29166
+ (goal) => {
29167
+ updatedGoal = goal;
29168
+ }
29169
+ ));
29170
+ if (updatedGoal !== null && this.goalPublishIsCurrent(sessionState, sessionGeneration)) {
29171
+ await this.publishGoalSnapshot(sessionState, toThreadGoalSnapshot(updatedGoal), false);
29172
+ }
29173
+ if (turnCompleted === null && updatedGoal !== null) {
29174
+ await this.startGoalContinuationIfCurrent(
29175
+ sessionState,
29176
+ sessionGeneration,
29177
+ goalControlGeneration,
29178
+ updatedGoal
29179
+ );
29180
+ }
28838
29181
  } else if (methodRequest.params.action === "clear") {
28839
29182
  await this.runWithProcessCheck(() => this.codexAcpClient.clearGoal(sessionState.sessionId));
28840
29183
  if (this.goalPublishIsCurrent(sessionState, sessionGeneration)) {
@@ -28934,6 +29277,11 @@ Check ${configPath} and project .codex directories, especially their config.toml
28934
29277
  getSessionGeneration(sessionId) {
28935
29278
  return this.sessionGenerations.get(sessionId) ?? 0;
28936
29279
  }
29280
+ bumpGoalControlGeneration(sessionId) {
29281
+ const generation = (this.goalControlGenerations.get(sessionId) ?? 0) + 1;
29282
+ this.goalControlGenerations.set(sessionId, generation);
29283
+ return generation;
29284
+ }
28937
29285
  bumpSessionGeneration(sessionId) {
28938
29286
  const generation = this.getSessionGeneration(sessionId) + 1;
28939
29287
  this.sessionGenerations.set(sessionId, generation);
@@ -29131,6 +29479,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
29131
29479
  this.pendingTurnStarts.delete(params.sessionId);
29132
29480
  this.activePrompts.delete(params.sessionId);
29133
29481
  this.steeringQueues.delete(params.sessionId);
29482
+ this.goalControlGenerations.delete(params.sessionId);
29134
29483
  }
29135
29484
  this.endSessionCloseFence(params.sessionId);
29136
29485
  }
@@ -29488,29 +29837,48 @@ Check ${configPath} and project .codex directories, especially their config.toml
29488
29837
  * fails or is cancelled before the turn starts.
29489
29838
  */
29490
29839
  async startNewTurnFromSteering(params) {
29840
+ await this.startNewTurnFromExternalPrompt(params, "Steering");
29841
+ return { outcome: "startedNewTurn" };
29842
+ }
29843
+ async startGoalContinuationIfCurrent(sessionState, sessionGeneration, goalControlGeneration, expectedGoal) {
29844
+ await this.startNewTurnFromExternalPrompt({
29845
+ sessionId: sessionState.sessionId,
29846
+ prompt: GOAL_CONTINUATION_PROMPT
29847
+ }, "Goal continuation", async () => {
29848
+ if (!this.goalPublishIsCurrent(sessionState, sessionGeneration) || this.goalControlGenerations.get(sessionState.sessionId) !== goalControlGeneration) {
29849
+ return false;
29850
+ }
29851
+ const currentGoal = await this.runWithProcessCheck(() => this.codexAcpClient.getGoal(sessionState.sessionId));
29852
+ return currentGoal?.status === "active" && currentGoal.objective === expectedGoal.objective && currentGoal.createdAt === expectedGoal.createdAt && this.goalControlGenerations.get(sessionState.sessionId) === goalControlGeneration;
29853
+ });
29854
+ }
29855
+ async startNewTurnFromExternalPrompt(params, source, canStart = async () => true) {
29491
29856
  const previousPrompt = this.activePrompts.get(params.sessionId);
29492
29857
  await previousPrompt?.completion;
29493
29858
  if (this.sessionIsClosing(params.sessionId)) {
29494
29859
  throw RequestError.invalidRequest(`Session ${params.sessionId} is closing`);
29495
29860
  }
29861
+ if (!await canStart()) {
29862
+ return false;
29863
+ }
29496
29864
  return await new Promise((resolve, reject) => {
29497
29865
  let turnStarted = false;
29498
29866
  const promptDone = this.prompt(params, void 0, () => {
29499
29867
  turnStarted = true;
29500
- logger.log("Steering session started a new turn", { sessionId: params.sessionId });
29501
- resolve({ outcome: "startedNewTurn" });
29868
+ logger.log(`${source} started a new turn`, { sessionId: params.sessionId });
29869
+ resolve(true);
29502
29870
  });
29503
29871
  promptDone.then(
29504
29872
  (response) => {
29505
29873
  if (!turnStarted && response.stopReason === "cancelled") {
29506
29874
  reject(RequestError.invalidRequest(`Session ${params.sessionId} was cancelled before the steering turn started`));
29507
29875
  } else {
29508
- resolve({ outcome: "startedNewTurn" });
29876
+ resolve(turnStarted);
29509
29877
  }
29510
29878
  },
29511
29879
  (error51) => {
29512
29880
  if (turnStarted) {
29513
- logger.error(`Steering-started prompt for session ${params.sessionId} failed`, error51);
29881
+ logger.error(`${source} prompt for session ${params.sessionId} failed`, error51);
29514
29882
  } else {
29515
29883
  reject(error51);
29516
29884
  }
@@ -29921,8 +30289,8 @@ Check ${configPath} and project .codex directories, especially their config.toml
29921
30289
  return `[@${name}](${uri})`;
29922
30290
  }
29923
30291
  if (uri.startsWith("file://")) {
29924
- const path6 = uri.replace("file://", "");
29925
- const fileName = path6.split("/").pop() ?? path6;
30292
+ const path7 = uri.replace("file://", "");
30293
+ const fileName = path7.split("/").pop() ?? path7;
29926
30294
  return `[@${fileName}](${uri})`;
29927
30295
  }
29928
30296
  return uri;
@@ -30175,6 +30543,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
30175
30543
  prompt: params.prompt
30176
30544
  });
30177
30545
  const sessionState = this.getSessionState(params.sessionId);
30546
+ let recoverableSessionFailure = sessionState.sessionFailure;
30178
30547
  sessionState.currentTurnId = null;
30179
30548
  sessionState.lastTokenUsage = null;
30180
30549
  const activePrompt = this.trackActivePrompt(params.sessionId);
@@ -30188,11 +30557,20 @@ Check ${configPath} and project .codex directories, especially their config.toml
30188
30557
  };
30189
30558
  const disposePromptRequestCancellation = this.observePromptRequestCancellation(signal, sessionState, activePrompt);
30190
30559
  let eventHandler = null;
30560
+ let promptNotificationsActive = true;
30561
+ const clearRecoveredSessionFailure = async (handler) => {
30562
+ const current = sessionState.sessionFailure;
30563
+ if (recoverableSessionFailure?.phase === "active" && current?.phase === "active" && current.id === recoverableSessionFailure.id && current.revision === recoverableSessionFailure.revision) {
30564
+ await handler.clearSessionFailure();
30565
+ }
30566
+ };
30191
30567
  try {
30192
30568
  const promptEventHandler = new CodexEventHandler(
30193
30569
  this.connection,
30194
30570
  sessionState,
30195
- clientSupportsPlanUpdates(this.clientCapabilities)
30571
+ clientSupportsPlanUpdates(this.clientCapabilities),
30572
+ clientSupportsTypedSessionFailures(this.clientCapabilities),
30573
+ this.sessionFailureEpoch
30196
30574
  );
30197
30575
  eventHandler = promptEventHandler;
30198
30576
  const approvalHandler = new CodexApprovalHandler(this.connection, sessionState, activePrompt.signal);
@@ -30205,8 +30583,16 @@ Check ${configPath} and project .codex directories, especially their config.toml
30205
30583
  await this.codexAcpClient.subscribeToSessionEvents(
30206
30584
  params.sessionId,
30207
30585
  async (event) => {
30586
+ if (!promptNotificationsActive) {
30587
+ await promptEventHandler.handleSessionScopedNotification(event);
30588
+ return;
30589
+ }
30590
+ const completesActiveTurn = event.method === "turn/completed" && event.params.turn.id === sessionState.currentTurnId;
30208
30591
  await elicitationHandler.handleNotification(event);
30209
- return promptEventHandler.handleNotification(event);
30592
+ await promptEventHandler.handleNotification(event);
30593
+ if (completesActiveTurn) {
30594
+ promptNotificationsActive = false;
30595
+ }
30210
30596
  },
30211
30597
  approvalHandler,
30212
30598
  elicitationHandler
@@ -30256,8 +30642,14 @@ Check ${configPath} and project .codex directories, especially their config.toml
30256
30642
  return this.cancelledPromptResponse(sessionState);
30257
30643
  }
30258
30644
  if (commandResult.handled) {
30645
+ promptNotificationsActive = false;
30259
30646
  logger.log("Prompt handled by a command");
30260
30647
  await this.codexAcpClient.waitForSessionNotifications(params.sessionId);
30648
+ await eventHandler.flushPendingErrors();
30649
+ await eventHandler.flushPendingErrorsAsSessionScoped();
30650
+ if (commandResult.turnCompleted) {
30651
+ await eventHandler.handleFailedTurn(commandResult.turnCompleted.turn);
30652
+ }
30261
30653
  if (commandResult.turnCompleted?.turn.status === "interrupted") {
30262
30654
  return this.cancelledPromptResponse(sessionState);
30263
30655
  }
@@ -30265,12 +30657,22 @@ Check ${configPath} and project .codex directories, especially their config.toml
30265
30657
  if (error52) {
30266
30658
  throw error52;
30267
30659
  }
30660
+ const terminalFailure2 = this.terminalFailurePromptResponse(
30661
+ sessionState,
30662
+ eventHandler,
30663
+ commandResult.turnCompleted?.turn.id ?? sessionState.currentTurnId
30664
+ );
30665
+ if (terminalFailure2) {
30666
+ return terminalFailure2;
30667
+ }
30668
+ await clearRecoveredSessionFailure(eventHandler);
30268
30669
  return {
30269
30670
  stopReason: "end_turn",
30270
30671
  usage: this.buildPromptUsage(sessionState.lastTokenUsage),
30271
30672
  _meta: this.buildQuotaMeta(sessionState)
30272
30673
  };
30273
30674
  }
30675
+ const effectiveParams = commandResult.prompt === void 0 ? params : { ...params, prompt: commandResult.prompt };
30274
30676
  if (this.sessionIsClosing(params.sessionId)) {
30275
30677
  return this.cancelledPromptResponse(sessionState);
30276
30678
  }
@@ -30283,7 +30685,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
30283
30685
  reason: sessionState.account?.type === "apiKey" ? "API key" : "model lacks reasoning"
30284
30686
  });
30285
30687
  }
30286
- if (!sessionState.supportedInputModalities.includes("image") && params.prompt.some((b) => b.type === "image")) {
30688
+ if (!sessionState.supportedInputModalities.includes("image") && effectiveParams.prompt.some((b) => b.type === "image")) {
30287
30689
  throw RequestError.invalidRequest("The current model does not support image input");
30288
30690
  }
30289
30691
  const agentMode = sessionState.agentMode;
@@ -30294,7 +30696,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
30294
30696
  ensurePendingTurnStart();
30295
30697
  const sendPromptPromise = this.runWithProcessCheck(
30296
30698
  () => this.codexAcpClient.sendPrompt(
30297
- params,
30699
+ effectiveParams,
30298
30700
  agentMode,
30299
30701
  modelId,
30300
30702
  serviceTier,
@@ -30329,6 +30731,9 @@ Check ${configPath} and project .codex directories, especially their config.toml
30329
30731
  return this.cancelledPromptResponse(sessionState);
30330
30732
  }
30331
30733
  await this.codexAcpClient.waitForSessionNotifications(params.sessionId);
30734
+ await eventHandler.flushPendingErrors();
30735
+ await eventHandler.handleFailedTurn(turnCompleted.turn);
30736
+ promptNotificationsActive = false;
30332
30737
  if (turnCompleted.turn.status === "interrupted") {
30333
30738
  await eventHandler.flushPendingPlanUpdates();
30334
30739
  return this.cancelledPromptResponse(sessionState);
@@ -30337,6 +30742,14 @@ Check ${configPath} and project .codex directories, especially their config.toml
30337
30742
  if (error51) {
30338
30743
  throw error51;
30339
30744
  }
30745
+ const terminalFailure = this.terminalFailurePromptResponse(
30746
+ sessionState,
30747
+ eventHandler,
30748
+ turnCompleted.turn.id
30749
+ );
30750
+ if (terminalFailure) {
30751
+ return terminalFailure;
30752
+ }
30340
30753
  await eventHandler.flushPendingPlanUpdates();
30341
30754
  const completedPlan = eventHandler.takeCompletedPlan();
30342
30755
  if (completedPlan !== null && sessionState.collaborationMode === PLAN_COLLABORATION_MODE && !this.promptShouldStop(params.sessionId, activePrompt)) {
@@ -30360,6 +30773,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
30360
30773
  prompt: [{ type: "text", text: "Implement the approved plan." }]
30361
30774
  };
30362
30775
  activePrompt.currentTurn = null;
30776
+ sessionState.currentTurnId = null;
30363
30777
  const implementationPromise = this.runWithProcessCheck(
30364
30778
  () => this.codexAcpClient.sendPrompt(
30365
30779
  implementationRequest,
@@ -30377,6 +30791,8 @@ Check ${configPath} and project .codex directories, especially their config.toml
30377
30791
  return;
30378
30792
  }
30379
30793
  sessionState.currentTurnId = turnId;
30794
+ recoverableSessionFailure = sessionState.sessionFailure;
30795
+ promptNotificationsActive = true;
30380
30796
  },
30381
30797
  () => this.promptShouldStop(params.sessionId, activePrompt)
30382
30798
  )
@@ -30395,6 +30811,9 @@ Check ${configPath} and project .codex directories, especially their config.toml
30395
30811
  return this.cancelledPromptResponse(sessionState);
30396
30812
  }
30397
30813
  await this.codexAcpClient.waitForSessionNotifications(params.sessionId);
30814
+ await eventHandler.flushPendingErrors();
30815
+ await eventHandler.handleFailedTurn(turnCompleted.turn);
30816
+ promptNotificationsActive = false;
30398
30817
  if (turnCompleted.turn.status === "interrupted") {
30399
30818
  await eventHandler.flushPendingPlanUpdates();
30400
30819
  return this.cancelledPromptResponse(sessionState);
@@ -30403,8 +30822,17 @@ Check ${configPath} and project .codex directories, especially their config.toml
30403
30822
  if (implementationError) {
30404
30823
  throw implementationError;
30405
30824
  }
30825
+ const implementationFailure = this.terminalFailurePromptResponse(
30826
+ sessionState,
30827
+ eventHandler,
30828
+ turnCompleted.turn.id
30829
+ );
30830
+ if (implementationFailure) {
30831
+ return implementationFailure;
30832
+ }
30406
30833
  }
30407
30834
  }
30835
+ await clearRecoveredSessionFailure(eventHandler);
30408
30836
  await this.publishFallbackSessionTitle(
30409
30837
  sessionState,
30410
30838
  this.createPromptFallbackTitle(params.prompt)
@@ -30416,8 +30844,27 @@ Check ${configPath} and project .codex directories, especially their config.toml
30416
30844
  };
30417
30845
  } catch (err) {
30418
30846
  logger.error(`Prompt for session ${params.sessionId} failed`, err);
30847
+ if (activePrompt.signal.aborted || this.sessionIsClosing(params.sessionId)) {
30848
+ return this.cancelledPromptResponse(sessionState);
30849
+ }
30850
+ const isProcessExit = err instanceof RequestError && err.code === CODEX_PROCESS_EXITED_ERROR_CODE;
30851
+ const isUnexpectedFailure = !(err instanceof RequestError);
30852
+ if (eventHandler !== null && clientSupportsTypedSessionFailures(this.clientCapabilities) && (isProcessExit || isUnexpectedFailure)) {
30853
+ const category = isProcessExit ? "transport_lost" : "internal_error";
30854
+ eventHandler.recordSyntheticTerminalFailure(category, sessionState.currentTurnId);
30855
+ const failureResponse = this.terminalFailurePromptResponse(
30856
+ sessionState,
30857
+ eventHandler,
30858
+ sessionState.currentTurnId,
30859
+ true
30860
+ );
30861
+ if (failureResponse !== null) {
30862
+ return failureResponse;
30863
+ }
30864
+ }
30419
30865
  throw err;
30420
30866
  } finally {
30867
+ promptNotificationsActive = false;
30421
30868
  logger.log("Prompt completed", { sessionId: params.sessionId });
30422
30869
  await eventHandler?.dispose();
30423
30870
  disposePromptRequestCancellation();
@@ -30488,6 +30935,20 @@ Check ${configPath} and project .codex directories, especially their config.toml
30488
30935
  _meta: this.buildQuotaMeta(sessionState)
30489
30936
  };
30490
30937
  }
30938
+ terminalFailurePromptResponse(sessionState, eventHandler, turnId, allowUnattributed = false) {
30939
+ const failureMeta = eventHandler.getTerminalSessionFailureMeta(turnId, allowUnattributed);
30940
+ if (failureMeta === null) {
30941
+ return null;
30942
+ }
30943
+ return {
30944
+ stopReason: "end_turn",
30945
+ usage: this.buildPromptUsage(sessionState.lastTokenUsage),
30946
+ _meta: {
30947
+ ...this.buildQuotaMeta(sessionState),
30948
+ ...failureMeta
30949
+ }
30950
+ };
30951
+ }
30491
30952
  buildQuotaMeta(sessionState) {
30492
30953
  const lastTokenUsage = sessionState.lastTokenUsage;
30493
30954
  const modelName = sessionState.currentModelId.replace(/\[.*?]$/, "");
@@ -30510,7 +30971,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
30510
30971
  return await operation();
30511
30972
  } catch (err) {
30512
30973
  const exitCode = this.getExitCode();
30513
- const requestErrorCode = 1001;
30974
+ const requestErrorCode = CODEX_PROCESS_EXITED_ERROR_CODE;
30514
30975
  if (exitCode == 3221225781) {
30515
30976
  throw new RequestError(requestErrorCode, `VC++ redistributable should be installed`);
30516
30977
  }
@@ -31532,10 +31993,17 @@ var sessionSteerParamsParser = external_exports.object({
31532
31993
  sessionId: external_exports.string(),
31533
31994
  prompt: external_exports.array(external_exports.any())
31534
31995
  }).passthrough();
31535
- var goalControlParamsParser = external_exports.object({
31536
- sessionId: external_exports.string(),
31537
- action: external_exports.enum(["pause", "resume", "clear"])
31538
- }).passthrough();
31996
+ var goalControlParamsParser = external_exports.discriminatedUnion("action", [
31997
+ external_exports.object({
31998
+ sessionId: external_exports.string(),
31999
+ action: external_exports.literal("set"),
32000
+ objective: external_exports.string().trim().min(1)
32001
+ }).passthrough(),
32002
+ external_exports.object({
32003
+ sessionId: external_exports.string(),
32004
+ action: external_exports.enum(["pause", "resume", "clear"])
32005
+ }).passthrough()
32006
+ ]);
31539
32007
  if (process.argv.includes("--version")) {
31540
32008
  console.log(`${package_default.name} ${package_default.version}`);
31541
32009
  process.exit(0);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.1.13",
6
+ "version": "1.2.0",
7
7
  "description": "",
8
8
  "main": "dist/index.js",
9
9
  "bin": {
@@ -36,6 +36,7 @@
36
36
  "example:steering": "node --import tsx examples/steering.ts",
37
37
  "example:steering:multistep": "node --import tsx examples/steering.ts",
38
38
  "generate-types": "./node_modules/.bin/codex app-server generate-ts --out src/app-server",
39
+ "release:preflight": "bash scripts/release-preflight.sh",
39
40
  "test": "vitest run",
40
41
  "test:e2e": "npm run build && RUN_E2E_TESTS=true vitest run src/__tests__/CodexACPAgent/e2e",
41
42
  "test:watch": "vitest",
@@ -64,7 +65,7 @@
64
65
  },
65
66
  "dependencies": {
66
67
  "@agentclientprotocol/sdk": "^1.3.0",
67
- "@openai/codex": "^0.146.1",
68
+ "@openai/codex": "^0.147.0",
68
69
  "diff": "^9.0.0",
69
70
  "open": "^11.0.0",
70
71
  "vscode-jsonrpc": "^9.0.1",