@agentclientprotocol/codex-acp 2.1.1 → 2.1.2-preview.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +1275 -403
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -27132,7 +27132,7 @@ import { createRequire } from "node:module";
27132
27132
  // src/StdUtils.ts
27133
27133
  var import_node = __toESM(require_main(), 1);
27134
27134
  import { StringDecoder } from "node:string_decoder";
27135
- import { Readable, Writable } from "node:stream";
27135
+ import { finished, Readable } from "node:stream";
27136
27136
 
27137
27137
  // src/Logger.ts
27138
27138
  import fs from "node:fs";
@@ -27264,9 +27264,37 @@ function createJSONRPCReader(readable) {
27264
27264
  };
27265
27265
  }
27266
27266
  function createJsonStream(readable, writable) {
27267
- const input2 = Writable.toWeb(writable);
27268
- const output2 = Readable.toWeb(readable);
27269
- return ndJsonStream2(input2, output2);
27267
+ const writeLine = createLineWriter(writable);
27268
+ const errorReplies = new WritableStream({ write: (chunk) => writeLine(chunk) });
27269
+ const input2 = Readable.toWeb(readable);
27270
+ const stream = ndJsonStream2(errorReplies, input2);
27271
+ const messages = new WritableStream({
27272
+ write: (message) => writeLine(JSON.stringify(message) + "\n")
27273
+ });
27274
+ return { readable: stream.readable, writable: messages };
27275
+ }
27276
+ function createLineWriter(writable) {
27277
+ let failure3;
27278
+ const waiting = /* @__PURE__ */ new Set();
27279
+ const stopWatching = finished(writable, (error62) => {
27280
+ stopWatching();
27281
+ writable.on("error", () => {
27282
+ });
27283
+ writable.off("drain", onDrain);
27284
+ failure3 = error62 ?? new Error("The output stream ended");
27285
+ for (const waiter of waiting) waiter.reject(failure3);
27286
+ waiting.clear();
27287
+ });
27288
+ const onDrain = () => {
27289
+ for (const waiter of waiting) waiter.resolve();
27290
+ waiting.clear();
27291
+ };
27292
+ writable.on("drain", onDrain);
27293
+ return (line) => {
27294
+ if (failure3 !== void 0) return Promise.reject(failure3);
27295
+ if (writable.write(line)) return Promise.resolve();
27296
+ return new Promise((resolve, reject) => waiting.add({ resolve, reject }));
27297
+ };
27270
27298
  }
27271
27299
  var LineSplitter = class {
27272
27300
  constructor(onLine) {
@@ -27298,6 +27326,17 @@ var LineSplitter = class {
27298
27326
  }
27299
27327
  }
27300
27328
  };
27329
+ async function settledWithin(promise2, ms) {
27330
+ let timer;
27331
+ const timeout = new Promise((resolve) => {
27332
+ timer = setTimeout(() => resolve("pending"), Math.max(0, ms));
27333
+ });
27334
+ try {
27335
+ return await Promise.race([promise2, timeout]);
27336
+ } finally {
27337
+ clearTimeout(timer);
27338
+ }
27339
+ }
27301
27340
 
27302
27341
  // src/CodexJsonRpcConnection.ts
27303
27342
  function startCodexConnection(codexPath, env) {
@@ -27357,6 +27396,17 @@ var ACPSessionConnection = class {
27357
27396
  }
27358
27397
  };
27359
27398
 
27399
+ // src/SessionTitle.ts
27400
+ var MAX_SESSION_TITLE_LENGTH = 256;
27401
+ function normalizeSessionTitle(title) {
27402
+ const normalized = title?.replace(/\s+/g, " ").trim() ?? "";
27403
+ if (normalized.length === 0) return null;
27404
+ if (normalized.length <= MAX_SESSION_TITLE_LENGTH) return normalized;
27405
+ let cut = normalized.slice(0, MAX_SESSION_TITLE_LENGTH - 1);
27406
+ if (/[\uD800-\uDBFF]$/.test(cut)) cut = cut.slice(0, -1);
27407
+ return `${cut.trimEnd()}\u2026`;
27408
+ }
27409
+
27360
27410
  // src/TokenCount.ts
27361
27411
  function toTokenCount(usage) {
27362
27412
  return {
@@ -27403,6 +27453,7 @@ var AIR_MESSAGE_PHASE_KEY = "phase";
27403
27453
  var AIR_GOAL_KEY = "goal";
27404
27454
  var AIR_KIND_KEY = "kind";
27405
27455
  var AIR_COMMAND_ACTION_KEY = "commandAction";
27456
+ var AIR_SKILL_PATH_KEY = "skillPath";
27406
27457
  var AIR_PERMISSION_KEY = "permission";
27407
27458
  var AIR_CONTEXT_COMPACTION_KEY = "contextCompaction";
27408
27459
  var AIR_RAW_INPUT_RENDERING_KEY = "rawInputRendering";
@@ -30424,12 +30475,12 @@ var CodexEventHandler = class {
30424
30475
  case "thread/tokenUsage/updated":
30425
30476
  return this.createUsageUpdate(notification.params);
30426
30477
  case "thread/name/updated":
30427
- this.sessionState.sessionTitle = notification.params.threadName ?? null;
30478
+ this.sessionState.sessionTitle = normalizeSessionTitle(notification.params.threadName);
30428
30479
  this.sessionState.sessionTitleSource = notification.params.threadName == null ? "unset" : "explicit";
30429
30480
  this.sessionState.titleGen?.observeRename();
30430
30481
  return {
30431
30482
  sessionUpdate: "session_info_update",
30432
- title: notification.params.threadName ?? null
30483
+ title: this.sessionState.sessionTitle
30433
30484
  };
30434
30485
  case "thread/status/changed":
30435
30486
  return this.createCodexSessionInfoUpdate({
@@ -32318,6 +32369,255 @@ function isCodexAuthRequest(request) {
32318
32369
  return request.methodId === "api-key" || request.methodId === "chat-gpt" || request.methodId === "chat-gpt-device-code" || request.methodId === "gateway";
32319
32370
  }
32320
32371
 
32372
+ // src/mcp/McpServerSignIn.ts
32373
+ import { randomUUID } from "node:crypto";
32374
+ function createMcpServerSignIn(connection, codexAcpClient, clientCapabilities) {
32375
+ return async (sessionId, serverName, signal) => {
32376
+ if (!clientSupportsUrlElicitation(clientCapabilities())) {
32377
+ return "unsupported";
32378
+ }
32379
+ try {
32380
+ return await signInWithUrlElicitation(connection, codexAcpClient(), sessionId, serverName, signal) ? "signedIn" : "failed";
32381
+ } catch (err) {
32382
+ if (!signal?.aborted) {
32383
+ logger.error(`Failed to authenticate MCP server ${serverName}`, err);
32384
+ }
32385
+ return "failed";
32386
+ }
32387
+ };
32388
+ }
32389
+ async function signInWithUrlElicitation(connection, codexAcpClient, sessionId, serverName, signal) {
32390
+ signal?.throwIfAborted();
32391
+ const stop = new AbortController();
32392
+ const onAbort = () => stop.abort(signal?.reason);
32393
+ signal?.addEventListener("abort", onAbort, { once: true });
32394
+ const stopped = new Promise((_, reject) => {
32395
+ stop.signal.addEventListener("abort", () => reject(stop.signal.reason), { once: true });
32396
+ });
32397
+ void stopped.catch(() => {
32398
+ });
32399
+ const elicitationId = `mcp-oauth-${randomUUID()}`;
32400
+ let dialogOpen = false;
32401
+ try {
32402
+ const completed = codexAcpClient.awaitMcpServerOauthLoginCompleted(serverName, sessionId, stop.signal);
32403
+ void completed.catch(() => {
32404
+ });
32405
+ const login2 = await Promise.race([
32406
+ codexAcpClient.mcpServerOauthLogin({ name: serverName, threadId: sessionId }),
32407
+ stopped
32408
+ ]);
32409
+ const elicitation = Promise.resolve(connection.request(
32410
+ methods.client.elicitation.create,
32411
+ {
32412
+ mode: "url",
32413
+ sessionId,
32414
+ message: `Authenticate with MCP server ${serverName}`,
32415
+ url: login2.authorizationUrl,
32416
+ elicitationId
32417
+ }
32418
+ ));
32419
+ void elicitation.catch(() => {
32420
+ });
32421
+ dialogOpen = true;
32422
+ const first = await Promise.race([
32423
+ completed.then((result2) => ({ type: "completed", result: result2 })),
32424
+ elicitation.then((response) => ({ type: "elicitation", response })),
32425
+ stopped
32426
+ ]);
32427
+ if (first.type === "elicitation" && !CreateElicitationResponse.isAccept(first.response)) {
32428
+ dialogOpen = false;
32429
+ return false;
32430
+ }
32431
+ const result = first.type === "completed" ? first.result : await Promise.race([completed, stopped]);
32432
+ return result.success;
32433
+ } finally {
32434
+ stop.abort();
32435
+ signal?.removeEventListener("abort", onAbort);
32436
+ if (dialogOpen) {
32437
+ await connection.notify(methods.client.elicitation.complete, { elicitationId });
32438
+ }
32439
+ }
32440
+ }
32441
+
32442
+ // src/McpServerName.ts
32443
+ var MCP_SERVER_NAME_WHITESPACE = /\p{White_Space}/gu;
32444
+ function sanitizeMcpServerName(name) {
32445
+ return name.replace(MCP_SERVER_NAME_WHITESPACE, "_");
32446
+ }
32447
+
32448
+ // src/tool-calls/reporters/McpStartupReporter.ts
32449
+ import { randomUUID as randomUUID2 } from "node:crypto";
32450
+ var McpStartupReporter = class {
32451
+ static skipped(serverNames) {
32452
+ return serverNames.map((server) => failure2(
32453
+ server,
32454
+ `[codex-acp] MCP server \`${server}\` was not started, because the Codex config already defines an MCP server with this name. Codex uses its own entry, or no server if that entry is disabled.`
32455
+ ));
32456
+ }
32457
+ static failures(event) {
32458
+ return [
32459
+ ...event.failed.map((server) => failure2(
32460
+ server.server,
32461
+ `[codex-acp forwarded startup error] MCP server \`${server.server}\` failed to start: ${server.error}`
32462
+ )),
32463
+ ...event.cancelled.map((server) => failure2(
32464
+ server,
32465
+ `[codex-acp forwarded startup error] MCP server \`${server}\` startup was cancelled.`
32466
+ ))
32467
+ ];
32468
+ }
32469
+ };
32470
+ function failure2(serverName, message) {
32471
+ return {
32472
+ // A unique id, so that a later report for the same server cannot replace this one.
32473
+ toolCallId: `mcp_startup.${encodeURIComponent(serverName)}.${randomUUID2()}`,
32474
+ report: "start",
32475
+ kind: "other",
32476
+ title: `mcp__${serverName}__startup`,
32477
+ status: "failed",
32478
+ result: [textContent(message)]
32479
+ };
32480
+ }
32481
+
32482
+ // src/mcp/McpSessionStartup.ts
32483
+ var McpSessionStartup = class {
32484
+ connection;
32485
+ codexAcpClient;
32486
+ runWithProcessCheck;
32487
+ signIn;
32488
+ capabilities;
32489
+ /** True when the session is open and does not close. */
32490
+ isSessionOpen;
32491
+ pendingSessions = /* @__PURE__ */ new Map();
32492
+ constructor(connection, codexAcpClient, runWithProcessCheck, signIn, capabilities, isSessionOpen) {
32493
+ this.connection = connection;
32494
+ this.codexAcpClient = codexAcpClient;
32495
+ this.runWithProcessCheck = runWithProcessCheck;
32496
+ this.signIn = signIn;
32497
+ this.capabilities = capabilities;
32498
+ this.isSessionOpen = isSessionOpen;
32499
+ }
32500
+ /**
32501
+ * Starts the startup wait of the session and publishes the report when the startup ends.
32502
+ * It returns a promise only when it waits for the startup, and returns null otherwise.
32503
+ * The promise rejects when the startup fails before `awaitTimeoutMs`. Then the session has no pending startup.
32504
+ */
32505
+ begin(sessionId, mcpServers, afterVersion, options) {
32506
+ const pendingStartup = this.createPendingSession(sessionId, mcpServers, afterVersion, options.skippedServers ?? []);
32507
+ if (options.publish) {
32508
+ this.pendingSessions.set(sessionId, pendingStartup);
32509
+ }
32510
+ const awaitTimeoutMs = options.awaitTimeoutMs;
32511
+ if (awaitTimeoutMs !== void 0 && awaitTimeoutMs > 0) {
32512
+ const startupWait = settledWithin(pendingStartup.startup, awaitTimeoutMs).then(() => void 0);
32513
+ void startupWait.then(
32514
+ () => this.publishOrAbort(sessionId, pendingStartup, options.publish),
32515
+ () => this.forget(sessionId, pendingStartup)
32516
+ );
32517
+ return startupWait;
32518
+ }
32519
+ this.publishOrAbort(sessionId, pendingStartup, options.publish);
32520
+ return null;
32521
+ }
32522
+ /** Stops the startup wait and the sign-in of the session. */
32523
+ close(sessionId) {
32524
+ this.pendingSessions.get(sessionId)?.abort.abort();
32525
+ this.pendingSessions.delete(sessionId);
32526
+ }
32527
+ isPending(sessionId) {
32528
+ return this.pendingSessions.has(sessionId);
32529
+ }
32530
+ publishOrAbort(sessionId, pendingStartup, publish) {
32531
+ if (publish) {
32532
+ this.publishAsync(sessionId);
32533
+ } else {
32534
+ pendingStartup.abort.abort();
32535
+ }
32536
+ }
32537
+ forget(sessionId, pendingStartup) {
32538
+ if (this.pendingSessions.get(sessionId) === pendingStartup) {
32539
+ this.pendingSessions.delete(sessionId);
32540
+ }
32541
+ }
32542
+ publishAsync(sessionId) {
32543
+ void this.doPublish(sessionId);
32544
+ }
32545
+ createPendingSession(sessionId, mcpServers, afterVersion, skippedServers) {
32546
+ const requestedServers = new Set(getRequestedMcpServerNames(mcpServers).filter((server) => !skippedServers.includes(server)));
32547
+ const abort = new AbortController();
32548
+ const startup = this.runWithProcessCheck(() => this.codexAcpClient().awaitMcpServerStartup(
32549
+ Array.from(requestedServers),
32550
+ afterVersion,
32551
+ { threadId: sessionId, signal: abort.signal }
32552
+ ));
32553
+ void startup.catch(() => {
32554
+ });
32555
+ return { requestedServers, skippedServers, startup, abort };
32556
+ }
32557
+ async doPublish(sessionId) {
32558
+ const pendingStartup = this.pendingSessions.get(sessionId);
32559
+ if (!pendingStartup) {
32560
+ return;
32561
+ }
32562
+ try {
32563
+ const mcpStartup = await pendingStartup.startup;
32564
+ if (!this.isSessionOpen(sessionId) || this.pendingSessions.get(sessionId) !== pendingStartup) {
32565
+ return;
32566
+ }
32567
+ await this.publish(sessionId, mcpStartup, pendingStartup);
32568
+ } catch (err) {
32569
+ if (!pendingStartup.abort.signal.aborted) {
32570
+ logger.error(`Failed to publish MCP startup status for session ${sessionId}`, err);
32571
+ }
32572
+ } finally {
32573
+ this.forget(sessionId, pendingStartup);
32574
+ }
32575
+ }
32576
+ async publish(sessionId, mcpStartup, pendingStartup) {
32577
+ const { requestedServers, skippedServers, abort: { signal } } = pendingStartup;
32578
+ const filteredStartup = {
32579
+ ready: mcpStartup.ready.filter((server) => requestedServers.has(server)),
32580
+ failed: mcpStartup.failed.filter((server) => requestedServers.has(server.server)),
32581
+ cancelled: mcpStartup.cancelled.filter((server) => requestedServers.has(server))
32582
+ };
32583
+ const failuresAfterOauth = [];
32584
+ const readyAfterOauth = [...filteredStartup.ready];
32585
+ for (const failure3 of filteredStartup.failed) {
32586
+ const signIn = failure3.failureReason === "reauthenticationRequired" ? await this.signIn(sessionId, failure3.server, signal) : "unsupported";
32587
+ if (signIn === "signedIn") {
32588
+ readyAfterOauth.push(failure3.server);
32589
+ } else {
32590
+ failuresAfterOauth.push(failure3);
32591
+ }
32592
+ }
32593
+ if (signal.aborted) {
32594
+ return;
32595
+ }
32596
+ const renderer = new AcpToolCallRenderer(this.capabilities());
32597
+ for (const facts4 of [
32598
+ ...McpStartupReporter.skipped(skippedServers),
32599
+ ...McpStartupReporter.failures({
32600
+ ...filteredStartup,
32601
+ ready: readyAfterOauth,
32602
+ failed: failuresAfterOauth
32603
+ })
32604
+ ]) {
32605
+ await this.connection.notify(methods.client.session.update, {
32606
+ sessionId,
32607
+ update: renderer.render(facts4)
32608
+ });
32609
+ }
32610
+ }
32611
+ };
32612
+ function getRequestedMcpServerNames(mcpServers) {
32613
+ return Array.from(new Set(mcpServers.map((server) => sanitizeMcpServerName(server.name))));
32614
+ }
32615
+ var MCP_STARTUP_AWAIT_TIMEOUT_META_KEY = "mcpStartupAwaitTimeoutMs";
32616
+ function parseMcpStartupAwaitTimeoutMs(meta3) {
32617
+ const value = meta3?.[MCP_STARTUP_AWAIT_TIMEOUT_META_KEY];
32618
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
32619
+ }
32620
+
32321
32621
  // node_modules/open/index.js
32322
32622
  import process10 from "node:process";
32323
32623
  import path5 from "node:path";
@@ -33161,10 +33461,70 @@ var AgentMode = class _AgentMode {
33161
33461
  // src/CodexAcpClient.ts
33162
33462
  import path7 from "node:path";
33163
33463
 
33164
- // src/McpServerName.ts
33165
- var MCP_SERVER_NAME_WHITESPACE = /\p{White_Space}/gu;
33166
- function sanitizeMcpServerName(name) {
33167
- return name.replace(MCP_SERVER_NAME_WHITESPACE, "_");
33464
+ // src/CodexThreadErrors.ts
33465
+ function errorText(err) {
33466
+ if (err instanceof Error) return err.message;
33467
+ if (typeof err === "string") return err;
33468
+ if (err !== null && typeof err === "object" && "message" in err) {
33469
+ return String(err.message);
33470
+ }
33471
+ return "";
33472
+ }
33473
+ function isMissingRolloutError(err) {
33474
+ return errorText(err).includes("no rollout found for thread id");
33475
+ }
33476
+ function isThreadNotLoadedError(err) {
33477
+ return errorText(err).includes("thread not loaded:");
33478
+ }
33479
+ function isThreadNotFoundError(err) {
33480
+ return errorText(err).includes("thread not found:");
33481
+ }
33482
+ function isInvalidThreadIdError(err) {
33483
+ const text = errorText(err);
33484
+ return text.includes("invalid thread id:") || text.includes("invalid session id:");
33485
+ }
33486
+ function isNoActiveTurnError(err) {
33487
+ return errorText(err).includes("no active turn to interrupt");
33488
+ }
33489
+ function isUnknownThreadError(err) {
33490
+ return isMissingRolloutError(err) || isThreadNotLoadedError(err) || isInvalidThreadIdError(err);
33491
+ }
33492
+ function isThreadActiveWriterError(err) {
33493
+ return /\bthread \S+ already has an active writer\b/.test(errorText(err));
33494
+ }
33495
+ function threadActiveWriterRequestError(threadId, err) {
33496
+ return RequestError.invalidRequest(
33497
+ { reason: "thread_active_writer", threadId, details: errorText(err) },
33498
+ "This Codex session is in use by another Codex client (the Codex app, the CLI or an IDE extension). Close the session there or quit that client, then try again."
33499
+ );
33500
+ }
33501
+ var INTERNAL_ERROR_CODE = -32603;
33502
+ var ACCOUNT_READ_UNAVAILABLE_MESSAGES = /* @__PURE__ */ new Set([
33503
+ "workspace routing discovery failed",
33504
+ "workspace routing discovery timed out"
33505
+ ]);
33506
+ var ACCOUNT_READ_AUTH_FAILURE_MESSAGES = /* @__PURE__ */ new Set([
33507
+ "workspace routing discovery unauthorized (401)",
33508
+ "selected workspace missing from routing discovery",
33509
+ "workspace routing requires a ChatGPT account id"
33510
+ ]);
33511
+ var ACCOUNT_READ_ACCOUNT_CHANGED_MESSAGE = "account changed during workspace routing discovery";
33512
+ function internalErrorText(err) {
33513
+ if (err === null || typeof err !== "object" || err.code !== INTERNAL_ERROR_CODE) {
33514
+ return null;
33515
+ }
33516
+ return errorText(err);
33517
+ }
33518
+ function isAccountReadUnavailableError(err) {
33519
+ const text = internalErrorText(err);
33520
+ return text !== null && ACCOUNT_READ_UNAVAILABLE_MESSAGES.has(text);
33521
+ }
33522
+ function isAccountReadAuthFailureError(err) {
33523
+ const text = internalErrorText(err);
33524
+ return text !== null && ACCOUNT_READ_AUTH_FAILURE_MESSAGES.has(text);
33525
+ }
33526
+ function isAccountReadAccountChangedError(err) {
33527
+ return internalErrorText(err) === ACCOUNT_READ_ACCOUNT_CHANGED_MESSAGE;
33168
33528
  }
33169
33529
 
33170
33530
  // package.json
@@ -33173,7 +33533,7 @@ var package_default = {
33173
33533
  publishConfig: {
33174
33534
  access: "public"
33175
33535
  },
33176
- version: "2.1.1",
33536
+ version: "2.1.2-preview.2",
33177
33537
  description: "",
33178
33538
  main: "dist/index.js",
33179
33539
  bin: {
@@ -33447,13 +33807,14 @@ import { createHash } from "node:crypto";
33447
33807
  async function forkSession(request, additionalDirectories, dependencies) {
33448
33808
  await dependencies.refreshSkills(request.cwd, additionalDirectories);
33449
33809
  const lastTurnId = await resolveForkTurnId(request, dependencies.codexClient);
33810
+ const sessionConfig = await dependencies.createSessionConfig(
33811
+ request.cwd,
33812
+ additionalDirectories,
33813
+ request.mcpServers ?? []
33814
+ );
33450
33815
  const response = await dependencies.codexClient.threadFork({
33451
33816
  excludeTurns: true,
33452
- config: await dependencies.createSessionConfig(
33453
- request.cwd,
33454
- additionalDirectories,
33455
- request.mcpServers ?? []
33456
- ),
33817
+ config: sessionConfig.config,
33457
33818
  cwd: request.cwd,
33458
33819
  ...lastTurnId !== void 0 && { lastTurnId },
33459
33820
  modelProvider: await dependencies.getResumeModelProvider(),
@@ -33468,7 +33829,8 @@ async function forkSession(request, additionalDirectories, dependencies) {
33468
33829
  collaborationMode: dependencies.getCollaborationMode(response.thread.id),
33469
33830
  modelProvider: response.modelProvider,
33470
33831
  currentServiceTier: response.serviceTier ?? null,
33471
- additionalDirectories
33832
+ additionalDirectories,
33833
+ skippedMcpServers: sessionConfig.skippedMcpServers
33472
33834
  };
33473
33835
  }
33474
33836
  async function resolveForkTurnId(request, codexClient) {
@@ -33525,41 +33887,6 @@ function isUnknownRecord(value) {
33525
33887
  return typeof value === "object" && value !== null && !Array.isArray(value);
33526
33888
  }
33527
33889
 
33528
- // src/CodexThreadErrors.ts
33529
- function errorText(err) {
33530
- if (err instanceof Error) return err.message;
33531
- if (typeof err === "string") return err;
33532
- if (err !== null && typeof err === "object" && "message" in err) {
33533
- return String(err.message);
33534
- }
33535
- return "";
33536
- }
33537
- function isMissingRolloutError(err) {
33538
- return errorText(err).includes("no rollout found for thread id");
33539
- }
33540
- function isThreadNotLoadedError(err) {
33541
- return errorText(err).includes("thread not loaded:");
33542
- }
33543
- function isInvalidThreadIdError(err) {
33544
- const text = errorText(err);
33545
- return text.includes("invalid thread id:") || text.includes("invalid session id:");
33546
- }
33547
- function isNoActiveTurnError(err) {
33548
- return errorText(err).includes("no active turn to interrupt");
33549
- }
33550
- function isUnknownThreadError(err) {
33551
- return isMissingRolloutError(err) || isThreadNotLoadedError(err) || isInvalidThreadIdError(err);
33552
- }
33553
- function isThreadActiveWriterError(err) {
33554
- return /\bthread \S+ already has an active writer\b/.test(errorText(err));
33555
- }
33556
- function threadActiveWriterRequestError(threadId, err) {
33557
- return RequestError.invalidRequest(
33558
- { reason: "thread_active_writer", threadId, details: errorText(err) },
33559
- "This Codex session is in use by another Codex client (the Codex app, the CLI or an IDE extension). Close the session there or quit that client, then try again."
33560
- );
33561
- }
33562
-
33563
33890
  // src/CodexAcpClient.ts
33564
33891
  var CUSTOM_GATEWAY_PROVIDER_ID = "custom-gateway";
33565
33892
  var OPENAI_PROVIDER_ID = "openai";
@@ -33645,8 +33972,7 @@ var CodexAcpClient = class {
33645
33972
  return result.success;
33646
33973
  }
33647
33974
  async authenticateWithChatGpt() {
33648
- const accountResponse = await this.codexClient.accountRead({ refreshToken: true });
33649
- if (accountResponse.account?.type === "chatgpt") {
33975
+ if (await this.hasWorkingChatGptLogin()) {
33650
33976
  return true;
33651
33977
  }
33652
33978
  const loginCompletedPromise = this.awaitNextLoginCompleted();
@@ -33657,9 +33983,32 @@ var CodexAcpClient = class {
33657
33983
  const result = await loginCompletedPromise;
33658
33984
  return result.success;
33659
33985
  }
33986
+ /**
33987
+ * Reads with a token refresh whether the agent has a ChatGPT login that a new login does not need to replace.
33988
+ *
33989
+ * An unavailable read answers `true`: only a ChatGPT login runs the routing discovery that failed, see
33990
+ * {@link isAccountReadUnavailableError}, and a new login cannot run without the network either.
33991
+ * A read that failed because the login does not work answers `false`, so a new login replaces it, see
33992
+ * {@link isAccountReadAuthFailureError}. Another error rejects.
33993
+ */
33994
+ async hasWorkingChatGptLogin() {
33995
+ try {
33996
+ const accountResponse = await this.codexClient.accountRead({ refreshToken: true });
33997
+ return accountResponse.account?.type === "chatgpt";
33998
+ } catch (error62) {
33999
+ if (isAccountReadUnavailableError(error62)) {
34000
+ logger.log("Account read unavailable, so the stored ChatGPT login stays", { error: String(error62) });
34001
+ return true;
34002
+ }
34003
+ if (isAccountReadAuthFailureError(error62)) {
34004
+ logger.log("The stored ChatGPT login does not work, so a new login starts", { error: String(error62) });
34005
+ return false;
34006
+ }
34007
+ throw error62;
34008
+ }
34009
+ }
33660
34010
  async authenticateWithChatGptDeviceCode(urlElicitationRequester) {
33661
- const accountResponse = await this.codexClient.accountRead({ refreshToken: true });
33662
- if (accountResponse.account?.type === "chatgpt") {
34011
+ if (await this.hasWorkingChatGptLogin()) {
33663
34012
  return true;
33664
34013
  }
33665
34014
  if (!urlElicitationRequester) {
@@ -33770,12 +34119,13 @@ var CodexAcpClient = class {
33770
34119
  await this.codexClient.accountLogout();
33771
34120
  await accountUpdatedPromise;
33772
34121
  }
33773
- async authRequired() {
34122
+ /** Reads whether the agent needs a login, with the account read that gave the answer. */
34123
+ async readAuthRequirement() {
33774
34124
  if (this.gatewayConfig != null) {
33775
- return false;
34125
+ return { required: false, account: null };
33776
34126
  }
33777
34127
  const response = await this.codexClient.accountRead({ refreshToken: false });
33778
- return response.requiresOpenaiAuth && !response.account;
34128
+ return { required: response.requiresOpenaiAuth && !response.account, account: response };
33779
34129
  }
33780
34130
  /**
33781
34131
  * Validates and stores custom gateway routing. Shared by the `gateway` auth
@@ -33986,9 +34336,10 @@ var CodexAcpClient = class {
33986
34336
  async resumeSession(request, onSubscribed) {
33987
34337
  const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta);
33988
34338
  await this.refreshSkills(request.cwd, additionalDirectories);
34339
+ const sessionConfig = await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers ?? []);
33989
34340
  const response = await this.resumeThread({
33990
34341
  excludeTurns: true,
33991
- config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers ?? []),
34342
+ config: sessionConfig.config,
33992
34343
  cwd: request.cwd,
33993
34344
  modelProvider: await this.getResumeModelProvider(),
33994
34345
  threadId: request.sessionId
@@ -34004,7 +34355,8 @@ var CodexAcpClient = class {
34004
34355
  collaborationMode: response.collaborationMode ?? this.getCollaborationMode(response.thread.id),
34005
34356
  modelProvider: response.modelProvider,
34006
34357
  currentServiceTier: response.serviceTier ?? null,
34007
- additionalDirectories
34358
+ additionalDirectories,
34359
+ skippedMcpServers: sessionConfig.skippedMcpServers
34008
34360
  };
34009
34361
  }
34010
34362
  async forkSession(request) {
@@ -34022,9 +34374,10 @@ var CodexAcpClient = class {
34022
34374
  async loadSession(request, onSubscribed) {
34023
34375
  const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta);
34024
34376
  await this.refreshSkills(request.cwd, additionalDirectories);
34377
+ const sessionConfig = await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers ?? []);
34025
34378
  const response = await this.resumeThread({
34026
34379
  excludeTurns: true,
34027
- config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers ?? []),
34380
+ config: sessionConfig.config,
34028
34381
  cwd: request.cwd,
34029
34382
  modelProvider: await this.getResumeModelProvider(),
34030
34383
  threadId: request.sessionId
@@ -34053,7 +34406,8 @@ var CodexAcpClient = class {
34053
34406
  currentServiceTier: response.serviceTier ?? null,
34054
34407
  thread,
34055
34408
  history,
34056
- additionalDirectories
34409
+ additionalDirectories,
34410
+ skippedMcpServers: sessionConfig.skippedMcpServers
34057
34411
  };
34058
34412
  }
34059
34413
  /**
@@ -34084,8 +34438,9 @@ var CodexAcpClient = class {
34084
34438
  async newSession(request) {
34085
34439
  const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta);
34086
34440
  await this.refreshSkills(request.cwd, additionalDirectories);
34441
+ const sessionConfig = await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers);
34087
34442
  const response = await this.codexClient.threadStart({
34088
- config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers),
34443
+ config: sessionConfig.config,
34089
34444
  modelProvider: this.getModelProvider(),
34090
34445
  cwd: request.cwd
34091
34446
  });
@@ -34101,7 +34456,8 @@ var CodexAcpClient = class {
34101
34456
  collaborationMode: this.getCollaborationMode(response.thread.id),
34102
34457
  modelProvider: response.modelProvider,
34103
34458
  currentServiceTier: response.serviceTier ?? null,
34104
- additionalDirectories
34459
+ additionalDirectories,
34460
+ skippedMcpServers: sessionConfig.skippedMcpServers
34105
34461
  };
34106
34462
  }
34107
34463
  async closeSession(sessionId) {
@@ -34178,11 +34534,11 @@ var CodexAcpClient = class {
34178
34534
  async clearGoal(sessionId) {
34179
34535
  await this.codexClient.runGoalClear({ threadId: sessionId });
34180
34536
  }
34181
- async awaitMcpServerStartup(serverNames, afterVersion) {
34182
- return await this.codexClient.awaitMcpServerStartup(serverNames, afterVersion);
34537
+ async awaitMcpServerStartup(serverNames, afterVersion, options) {
34538
+ return await this.codexClient.mcpStartup.await(serverNames, afterVersion, options);
34183
34539
  }
34184
34540
  getMcpServerStartupVersion() {
34185
- return this.codexClient.getMcpServerStartupVersion();
34541
+ return this.codexClient.mcpStartup.version();
34186
34542
  }
34187
34543
  async createSessionConfig(projectPath, additionalDirectories, mcpServers) {
34188
34544
  const sessionRoots = [projectPath, ...additionalDirectories];
@@ -34205,7 +34561,7 @@ var CodexAcpClient = class {
34205
34561
  };
34206
34562
  const configWithWorkspaceRoots = mergeSandboxWorkspaceWriteRoots(mergedConfig, additionalDirectories);
34207
34563
  if (mcpServers.length === 0) {
34208
- return configWithWorkspaceRoots;
34564
+ return { config: configWithWorkspaceRoots, skippedMcpServers: [] };
34209
34565
  }
34210
34566
  const requestedServers = mcpServers.map((mcp) => ({
34211
34567
  name: sanitizeMcpServerName(mcp.name),
@@ -34216,12 +34572,22 @@ var CodexAcpClient = class {
34216
34572
  const existingNames = await this.getConfigMcpServerNames(projectPath);
34217
34573
  serversToConfigure = requestedServers.filter((mcp) => !existingNames.has(mcp.name));
34218
34574
  }
34219
- if (serversToConfigure.length === 0) {
34220
- return configWithWorkspaceRoots;
34575
+ const skippedMcpServers = requestedServers.filter((mcp) => !serversToConfigure.includes(mcp)).map((mcp) => mcp.name);
34576
+ if (skippedMcpServers.length > 0) {
34577
+ logger.log("Skipping requested MCP servers that the Codex config already defines", {
34578
+ projectPath,
34579
+ skippedMcpServers
34580
+ });
34581
+ }
34582
+ if (serversToConfigure.length === 0) {
34583
+ return { config: configWithWorkspaceRoots, skippedMcpServers };
34221
34584
  }
34222
34585
  return {
34223
- ...configWithWorkspaceRoots,
34224
- "mcp_servers": Object.fromEntries(serversToConfigure.map((mcp) => [mcp.name, this.createMcpSeverConfig(mcp.server)]))
34586
+ config: {
34587
+ ...configWithWorkspaceRoots,
34588
+ "mcp_servers": Object.fromEntries(serversToConfigure.map((mcp) => [mcp.name, this.createMcpSeverConfig(mcp.server)]))
34589
+ },
34590
+ skippedMcpServers
34225
34591
  };
34226
34592
  }
34227
34593
  async getConfigMcpServerNames(projectPath) {
@@ -34409,14 +34775,18 @@ var CodexAcpClient = class {
34409
34775
  });
34410
34776
  });
34411
34777
  }
34412
- async listMcpServers() {
34413
- return this.codexClient.listMcpServerStatus({});
34778
+ async listMcpServers(params) {
34779
+ return this.codexClient.listMcpServerStatus(params);
34780
+ }
34781
+ /** Reloads the MCP configuration. Codex reconnects the servers of every loaded thread that failed, closed, or changed. */
34782
+ async reloadMcpServers() {
34783
+ await this.codexClient.mcpServerReload();
34414
34784
  }
34415
34785
  async mcpServerOauthLogin(params) {
34416
34786
  return await this.codexClient.mcpServerOauthLogin(params);
34417
34787
  }
34418
- async awaitMcpServerOauthLoginCompleted(name, threadId) {
34419
- return await this.codexClient.awaitMcpServerOauthLoginCompleted(name, threadId);
34788
+ async awaitMcpServerOauthLoginCompleted(name, threadId, signal) {
34789
+ return await this.codexClient.mcpOauthCompletions.await(name, threadId, signal);
34420
34790
  }
34421
34791
  async listSessions(request) {
34422
34792
  const sourceKinds = [
@@ -34445,7 +34815,7 @@ var CodexAcpClient = class {
34445
34815
  const mapThreadToSession = (thread) => ({
34446
34816
  sessionId: thread.id,
34447
34817
  cwd: thread.cwd,
34448
- title: (thread.name ?? thread.preview) || null,
34818
+ title: normalizeSessionTitle(thread.name ?? thread.preview),
34449
34819
  updatedAt: new Date(thread.updatedAt * 1e3).toISOString()
34450
34820
  });
34451
34821
  let sessions = listResponse.data.map(mapThreadToSession);
@@ -34658,6 +35028,204 @@ async function* oneItemPage(items) {
34658
35028
 
34659
35029
  // src/CodexAppServerClient.ts
34660
35030
  var import_node2 = __toESM(require_main(), 1);
35031
+
35032
+ // src/mcp/McpOauthCompletions.ts
35033
+ var McpOauthCompletions = class {
35034
+ /** The waiters, by server name. */
35035
+ waiters = /* @__PURE__ */ new Map();
35036
+ closedError = null;
35037
+ /** Completes the waiters of the server. A notification without a thread completes the waiters of every thread. */
35038
+ complete(event) {
35039
+ for (const waiter of [...this.waiters.get(event.name) ?? []]) {
35040
+ if (event.threadId === null || event.threadId === waiter.threadId) {
35041
+ waiter.resolve(event);
35042
+ }
35043
+ }
35044
+ }
35045
+ /** Waits for the sign-in completion of the server. The wait rejects when `signal` aborts or when the Codex connection closes. */
35046
+ async await(name, threadId, signal) {
35047
+ signal?.throwIfAborted();
35048
+ if (this.closedError !== null) {
35049
+ throw this.closedError;
35050
+ }
35051
+ return await new Promise((resolve, reject) => {
35052
+ const onAbort = () => waiter.reject(signal?.reason);
35053
+ const release = () => {
35054
+ const waiters2 = this.waiters.get(name);
35055
+ waiters2?.delete(waiter);
35056
+ if (waiters2?.size === 0) {
35057
+ this.waiters.delete(name);
35058
+ }
35059
+ signal?.removeEventListener("abort", onAbort);
35060
+ };
35061
+ const waiter = {
35062
+ threadId,
35063
+ resolve: (event) => {
35064
+ release();
35065
+ resolve(event);
35066
+ },
35067
+ reject: (error62) => {
35068
+ release();
35069
+ reject(error62);
35070
+ }
35071
+ };
35072
+ signal?.addEventListener("abort", onAbort, { once: true });
35073
+ let waiters = this.waiters.get(name);
35074
+ if (waiters === void 0) {
35075
+ waiters = /* @__PURE__ */ new Set();
35076
+ this.waiters.set(name, waiters);
35077
+ }
35078
+ waiters.add(waiter);
35079
+ });
35080
+ }
35081
+ /** Rejects every wait, now and later, because the Codex connection closed. */
35082
+ dispose() {
35083
+ this.closedError ??= new Error("Codex connection closed during the MCP server sign-in.");
35084
+ for (const waiters of [...this.waiters.values()]) {
35085
+ for (const waiter of [...waiters]) {
35086
+ waiter.reject(this.closedError);
35087
+ }
35088
+ }
35089
+ }
35090
+ };
35091
+
35092
+ // src/mcp/McpStartupTracker.ts
35093
+ var McpStartupTracker = class {
35094
+ currentVersion = 0;
35095
+ /** The last startup state of each MCP server, by server name and then by thread id. A `null` thread id is global. */
35096
+ states = /* @__PURE__ */ new Map();
35097
+ waiters = [];
35098
+ closedError = null;
35099
+ /** Returns the version of the last startup event. */
35100
+ version() {
35101
+ return this.currentVersion;
35102
+ }
35103
+ record(params) {
35104
+ this.currentVersion += 1;
35105
+ let states = this.states.get(params.name);
35106
+ if (states === void 0) {
35107
+ states = /* @__PURE__ */ new Map();
35108
+ this.states.set(params.name, states);
35109
+ }
35110
+ states.set(params.threadId ?? null, {
35111
+ status: params.status,
35112
+ error: params.error,
35113
+ failureReason: params.failureReason ?? null,
35114
+ version: this.currentVersion
35115
+ });
35116
+ for (const waiter of [...this.waiters]) {
35117
+ const result = this.tryBuildResult(waiter.serverNames, waiter.afterVersion, waiter.threadId);
35118
+ if (result !== null) {
35119
+ waiter.resolve(result);
35120
+ }
35121
+ }
35122
+ }
35123
+ /**
35124
+ * Waits until each server reaches a terminal startup state after `afterVersion`.
35125
+ * Only the events of `threadId` and the global events count.
35126
+ * The wait rejects when `signal` aborts or when the Codex connection closes.
35127
+ */
35128
+ async await(serverNames, afterVersion, options) {
35129
+ const uniqueServerNames = Array.from(new Set(serverNames.map((name) => name.trim()).filter((name) => name.length > 0)));
35130
+ if (uniqueServerNames.length === 0) {
35131
+ return { ready: [], failed: [], cancelled: [] };
35132
+ }
35133
+ const { threadId, signal } = options;
35134
+ const result = this.tryBuildResult(uniqueServerNames, afterVersion, threadId);
35135
+ if (result !== null) {
35136
+ return result;
35137
+ }
35138
+ signal?.throwIfAborted();
35139
+ if (this.closedError !== null) {
35140
+ throw this.closedError;
35141
+ }
35142
+ return await new Promise((resolve, reject) => {
35143
+ const onAbort = () => waiter.reject(signal?.reason);
35144
+ const release = () => {
35145
+ const index = this.waiters.indexOf(waiter);
35146
+ if (index >= 0) {
35147
+ this.waiters.splice(index, 1);
35148
+ }
35149
+ signal?.removeEventListener("abort", onAbort);
35150
+ };
35151
+ const waiter = {
35152
+ serverNames: uniqueServerNames,
35153
+ afterVersion,
35154
+ threadId,
35155
+ resolve: (startup) => {
35156
+ release();
35157
+ resolve(startup);
35158
+ },
35159
+ reject: (error62) => {
35160
+ release();
35161
+ reject(error62);
35162
+ }
35163
+ };
35164
+ signal?.addEventListener("abort", onAbort, { once: true });
35165
+ this.waiters.push(waiter);
35166
+ });
35167
+ }
35168
+ /** Drops the startup states of a thread. The global states stay. */
35169
+ forgetThread(threadId) {
35170
+ for (const [serverName, states] of this.states) {
35171
+ states.delete(threadId);
35172
+ if (states.size === 0) {
35173
+ this.states.delete(serverName);
35174
+ }
35175
+ }
35176
+ }
35177
+ /** Rejects every wait, now and later, because the Codex connection closed. */
35178
+ dispose() {
35179
+ this.closedError ??= new Error("Codex connection closed during the MCP server startup.");
35180
+ for (const waiter of [...this.waiters]) {
35181
+ waiter.reject(this.closedError);
35182
+ }
35183
+ }
35184
+ /** Returns the latest startup state of the server for the thread. A global state also counts. */
35185
+ latestState(serverName, threadId) {
35186
+ let latest;
35187
+ for (const [stateThreadId, state] of this.states.get(serverName) ?? []) {
35188
+ if (stateThreadId !== null && stateThreadId !== threadId) {
35189
+ continue;
35190
+ }
35191
+ if (latest === void 0 || state.version > latest.version) {
35192
+ latest = state;
35193
+ }
35194
+ }
35195
+ return latest;
35196
+ }
35197
+ tryBuildResult(serverNames, afterVersion, threadId) {
35198
+ const ready = [];
35199
+ const failed = [];
35200
+ const cancelled = [];
35201
+ for (const serverName of serverNames) {
35202
+ const state = this.latestState(serverName, threadId);
35203
+ if (!state || state.version <= afterVersion) {
35204
+ return null;
35205
+ }
35206
+ switch (state.status) {
35207
+ case "starting":
35208
+ return null;
35209
+ case "ready":
35210
+ ready.push(serverName);
35211
+ break;
35212
+ case "failed":
35213
+ failed.push({
35214
+ server: serverName,
35215
+ error: state.error ?? "unknown MCP startup error",
35216
+ ...state.failureReason === null ? {} : { failureReason: state.failureReason }
35217
+ });
35218
+ break;
35219
+ case "cancelled":
35220
+ cancelled.push(serverName);
35221
+ break;
35222
+ }
35223
+ }
35224
+ return { ready, failed, cancelled };
35225
+ }
35226
+ };
35227
+
35228
+ // src/CodexAppServerClient.ts
34661
35229
  var CommandExecutionApprovalRequest = new import_node2.RequestType("item/commandExecution/requestApproval");
34662
35230
  var FileChangeApprovalRequest = new import_node2.RequestType("item/fileChange/requestApproval");
34663
35231
  var PermissionsApprovalRequest = new import_node2.RequestType("item/permissions/requestApproval");
@@ -34671,9 +35239,8 @@ var CodexAppServerClient = class {
34671
35239
  connection;
34672
35240
  approvalHandlers = /* @__PURE__ */ new Map();
34673
35241
  elicitationHandlers = /* @__PURE__ */ new Map();
34674
- mcpServerStartupVersion = 0;
34675
- mcpServerStartupStates = /* @__PURE__ */ new Map();
34676
- mcpServerStartupResolvers = [];
35242
+ mcpStartup = new McpStartupTracker();
35243
+ mcpOauthCompletions = new McpOauthCompletions();
34677
35244
  pendingTurnCompletionResolvers = /* @__PURE__ */ new Map();
34678
35245
  pendingCompactionCompletionResolvers = /* @__PURE__ */ new Map();
34679
35246
  turnCompletionCaptures = /* @__PURE__ */ new Map();
@@ -34685,17 +35252,15 @@ var CodexAppServerClient = class {
34685
35252
  staleTurnIds = /* @__PURE__ */ new Map();
34686
35253
  constructor(connection) {
34687
35254
  this.connection = connection;
35255
+ this.connection.onClose?.(() => this.disposeMcpWaits());
35256
+ this.connection.onDispose?.(() => this.disposeMcpWaits());
34688
35257
  this.connection.onUnhandledNotification((data) => {
34689
35258
  const serverNotification = data;
34690
35259
  if (isMcpServerStatusUpdatedNotification(serverNotification)) {
34691
- this.mcpServerStartupVersion += 1;
34692
- this.mcpServerStartupStates.set(serverNotification.params.name, {
34693
- status: serverNotification.params.status,
34694
- error: serverNotification.params.error,
34695
- failureReason: serverNotification.params.failureReason ?? null,
34696
- version: this.mcpServerStartupVersion
34697
- });
34698
- this.resolveMcpServerStartupResolvers();
35260
+ this.mcpStartup.record(serverNotification.params);
35261
+ }
35262
+ if (serverNotification.method === "mcpServer/oauthLogin/completed") {
35263
+ this.mcpOauthCompletions.complete(serverNotification.params);
34699
35264
  }
34700
35265
  if (isTurnCompletedNotification(serverNotification)) {
34701
35266
  this.recordTurnCompleted(serverNotification.params);
@@ -35014,7 +35579,9 @@ var CodexAppServerClient = class {
35014
35579
  });
35015
35580
  void completed.catch(() => {
35016
35581
  });
35017
- const closed = this.connection.onClose?.(() => rejectCompleted(new Error("Codex connection closed during compaction.")));
35582
+ const rejectClosed = () => rejectCompleted(new Error("Codex connection closed during compaction."));
35583
+ const closed = this.connection.onClose?.(rejectClosed);
35584
+ const disposed = this.connection.onDispose?.(rejectClosed);
35018
35585
  const completeCompaction = (event) => {
35019
35586
  if (compactTurnId !== null && event.params.turnId !== compactTurnId) return;
35020
35587
  resolveCompleted(event);
@@ -35040,6 +35607,7 @@ var CodexAppServerClient = class {
35040
35607
  releaseRoutingCapture();
35041
35608
  releaseCompactionCapture();
35042
35609
  closed?.dispose();
35610
+ disposed?.dispose();
35043
35611
  }
35044
35612
  }
35045
35613
  async turnInterrupt(params) {
@@ -35119,7 +35687,7 @@ var CodexAppServerClient = class {
35119
35687
  cursor,
35120
35688
  limit: HISTORY_PAGE_ITEMS,
35121
35689
  sortDirection: "asc"
35122
- }));
35690
+ }), (page) => page.some((entry) => entry.item.id === lastItemId));
35123
35691
  for await (const page of pages) {
35124
35692
  const items = page.map((entry) => entry.item);
35125
35693
  const lastIndex = items.findIndex((item) => item.id === lastItemId);
@@ -35193,24 +35761,12 @@ var CodexAppServerClient = class {
35193
35761
  async listMcpServerStatus(params) {
35194
35762
  return await this.sendRequest({ method: "mcpServerStatus/list", params });
35195
35763
  }
35764
+ async mcpServerReload() {
35765
+ return await this.sendRequest({ method: "config/mcpServer/reload", params: void 0 });
35766
+ }
35196
35767
  async mcpServerOauthLogin(params) {
35197
35768
  return await this.sendRequest({ method: "mcpServer/oauth/login", params });
35198
35769
  }
35199
- async awaitMcpServerOauthLoginCompleted(name, threadId) {
35200
- return await new Promise((resolve) => {
35201
- let disposable;
35202
- disposable = this.connection.onNotification(
35203
- "mcpServer/oauthLogin/completed",
35204
- (event) => {
35205
- if (event.name !== name || event.threadId !== threadId) {
35206
- return;
35207
- }
35208
- disposable?.dispose();
35209
- resolve(event);
35210
- }
35211
- );
35212
- });
35213
- }
35214
35770
  async accountLogin(params) {
35215
35771
  return await this.sendRequest({ method: "account/login/start", params });
35216
35772
  }
@@ -35223,26 +35779,6 @@ var CodexAppServerClient = class {
35223
35779
  async configRead(params) {
35224
35780
  return await this.sendRequest({ method: "config/read", params });
35225
35781
  }
35226
- getMcpServerStartupVersion() {
35227
- return this.mcpServerStartupVersion;
35228
- }
35229
- async awaitMcpServerStartup(serverNames, afterVersion) {
35230
- const uniqueServerNames = Array.from(new Set(serverNames.map((serverName) => serverName.trim()).filter((serverName) => serverName.length > 0)));
35231
- if (uniqueServerNames.length === 0) {
35232
- return { ready: [], failed: [], cancelled: [] };
35233
- }
35234
- const result = this.tryBuildMcpStartupResult(uniqueServerNames, afterVersion);
35235
- if (result !== null) {
35236
- return result;
35237
- }
35238
- return await new Promise((resolve) => {
35239
- this.mcpServerStartupResolvers.push({
35240
- serverNames: uniqueServerNames,
35241
- afterVersion,
35242
- resolve
35243
- });
35244
- });
35245
- }
35246
35782
  async accountRead(params) {
35247
35783
  return await this.sendRequest({ method: "account/read", params });
35248
35784
  }
@@ -35515,46 +36051,9 @@ var CodexAppServerClient = class {
35515
36051
  }
35516
36052
  };
35517
36053
  }
35518
- resolveMcpServerStartupResolvers() {
35519
- const pendingResolvers = [];
35520
- for (const resolver of this.mcpServerStartupResolvers) {
35521
- const result = this.tryBuildMcpStartupResult(resolver.serverNames, resolver.afterVersion);
35522
- if (result !== null) {
35523
- resolver.resolve(result);
35524
- } else {
35525
- pendingResolvers.push(resolver);
35526
- }
35527
- }
35528
- this.mcpServerStartupResolvers.splice(0, this.mcpServerStartupResolvers.length, ...pendingResolvers);
35529
- }
35530
- tryBuildMcpStartupResult(serverNames, afterVersion) {
35531
- const ready = [];
35532
- const failed = [];
35533
- const cancelled = [];
35534
- for (const serverName of serverNames) {
35535
- const state = this.mcpServerStartupStates.get(serverName);
35536
- if (!state || state.version <= afterVersion) {
35537
- return null;
35538
- }
35539
- switch (state.status) {
35540
- case "starting":
35541
- return null;
35542
- case "ready":
35543
- ready.push(serverName);
35544
- break;
35545
- case "failed":
35546
- failed.push({
35547
- server: serverName,
35548
- error: state.error ?? "unknown MCP startup error",
35549
- ...state.failureReason === null ? {} : { failureReason: state.failureReason }
35550
- });
35551
- break;
35552
- case "cancelled":
35553
- cancelled.push(serverName);
35554
- break;
35555
- }
35556
- }
35557
- return { ready, failed, cancelled };
36054
+ disposeMcpWaits() {
36055
+ this.mcpStartup.dispose();
36056
+ this.mcpOauthCompletions.dispose();
35558
36057
  }
35559
36058
  async sendRequest(request) {
35560
36059
  for (const callback of this.codexEventHandlers) {
@@ -35614,18 +36113,28 @@ function extractTurnRouting(notification) {
35614
36113
  }
35615
36114
  return { threadId, turnId: null };
35616
36115
  }
35617
- async function* historyPages(threadId, method, requestPage) {
36116
+ async function* historyPages(threadId, method, requestPage, endsHistory) {
36117
+ const request = (cursor2) => {
36118
+ const page = withHistoryTimeout(requestPage(cursor2), method, threadId);
36119
+ page.catch(() => {
36120
+ });
36121
+ return page;
36122
+ };
35618
36123
  const seenCursors = /* @__PURE__ */ new Set();
35619
36124
  let cursor = null;
36125
+ let next = null;
35620
36126
  do {
35621
- const page = await withHistoryTimeout(requestPage(cursor), method, threadId);
35622
- yield page.data;
36127
+ const page = await (next ?? request(cursor));
36128
+ next = null;
35623
36129
  cursor = page.nextCursor;
35624
- if (cursor !== null) {
35625
- if (seenCursors.has(cursor)) {
35626
- throw new Error("Codex returned a repeated thread history cursor");
35627
- }
36130
+ const repeated = cursor !== null && seenCursors.has(cursor);
36131
+ if (cursor !== null && !repeated) {
35628
36132
  seenCursors.add(cursor);
36133
+ if (endsHistory && !endsHistory(page.data)) next = request(cursor);
36134
+ }
36135
+ yield page.data;
36136
+ if (repeated) {
36137
+ throw new Error("Codex returned a repeated thread history cursor");
35629
36138
  }
35630
36139
  } while (cursor !== null);
35631
36140
  }
@@ -35700,6 +36209,338 @@ function createReasoningEffortConfigOption(supportedReasoningEfforts, currentEff
35700
36209
  };
35701
36210
  }
35702
36211
 
36212
+ // src/mcp/McpServerStatusList.ts
36213
+ async function listMcpServerStatus(codexAcpClient, threadId) {
36214
+ const servers = [];
36215
+ const seenCursors = /* @__PURE__ */ new Set();
36216
+ let cursor = null;
36217
+ do {
36218
+ const page = await codexAcpClient.listMcpServers({
36219
+ cursor,
36220
+ detail: "toolsAndAuthOnly",
36221
+ ...threadId === void 0 ? {} : { threadId }
36222
+ });
36223
+ servers.push(...page.data);
36224
+ cursor = page.nextCursor;
36225
+ if (cursor !== null) {
36226
+ if (seenCursors.has(cursor)) {
36227
+ logger.log(`MCP server status list repeated the cursor ${cursor}`);
36228
+ break;
36229
+ }
36230
+ seenCursors.add(cursor);
36231
+ }
36232
+ } while (cursor !== null);
36233
+ return servers;
36234
+ }
36235
+
36236
+ // src/mcp/McpStatusMarkdown.ts
36237
+ var NO_SERVERS_MESSAGE = "No MCP servers are configured.";
36238
+ var SUMMARY_ORDER = [
36239
+ "connected",
36240
+ "starting",
36241
+ "notStarted",
36242
+ "authenticationRequired",
36243
+ "failed",
36244
+ "cancelled",
36245
+ "disabled",
36246
+ null
36247
+ ];
36248
+ var GROUP_ORDER = [
36249
+ "failed",
36250
+ "cancelled",
36251
+ "authenticationRequired",
36252
+ "notStarted",
36253
+ "starting",
36254
+ "connected",
36255
+ null
36256
+ ];
36257
+ function statusLabel(status) {
36258
+ switch (status) {
36259
+ case "connected":
36260
+ return "Connected";
36261
+ case "starting":
36262
+ return "Connecting";
36263
+ case "notStarted":
36264
+ return "Not started";
36265
+ case "authenticationRequired":
36266
+ return "Needs authentication";
36267
+ case "failed":
36268
+ return "Failed";
36269
+ case "cancelled":
36270
+ return "Cancelled";
36271
+ case "disabled":
36272
+ return "Disabled";
36273
+ case null:
36274
+ return "Unknown";
36275
+ }
36276
+ }
36277
+ var KNOWN_STATUSES = /* @__PURE__ */ new Set([...GROUP_ORDER.filter((status) => status !== null), "disabled"]);
36278
+ function toEntry(server) {
36279
+ const status = server.runtimeStatus;
36280
+ return {
36281
+ name: server.name,
36282
+ status: status !== null && KNOWN_STATUSES.has(status) ? status : null,
36283
+ toolCount: Object.keys(server.tools ?? {}).length,
36284
+ toolsError: server.toolsError,
36285
+ authStatus: server.authStatus,
36286
+ listedByCodex: true
36287
+ };
36288
+ }
36289
+ function formatStatus(servers, errors) {
36290
+ if (servers.length === 0) {
36291
+ return NO_SERVERS_MESSAGE;
36292
+ }
36293
+ const counts = SUMMARY_ORDER.map((status) => ({ status, count: servers.filter((server) => server.status === status).length })).filter((entry) => entry.count > 0).map((entry) => `${entry.count} ${statusLabel(entry.status).toLowerCase()}`);
36294
+ const lines = [`**MCP servers:** ${servers.length} (${counts.join(", ")})`];
36295
+ for (const status of GROUP_ORDER) {
36296
+ const group = servers.filter((server) => server.status === status);
36297
+ if (group.length === 0) {
36298
+ continue;
36299
+ }
36300
+ lines.push("", `**${statusLabel(status)}**`);
36301
+ for (const server of group) {
36302
+ const detail = serverDetail(server, errors.get(server.name) ?? null);
36303
+ lines.push(detail === null ? `- ${inlineCode(server.name)}` : `- ${inlineCode(server.name)}: ${detail}`);
36304
+ }
36305
+ }
36306
+ const disabled = servers.filter((server) => server.status === "disabled");
36307
+ if (disabled.length > 0) {
36308
+ lines.push("", `**Disabled:** ${disabled.map((server) => inlineCode(server.name)).join(", ")}`);
36309
+ }
36310
+ if (servers.some((server) => server.status !== null && RECONNECT_HINT_STATUSES.has(server.status))) {
36311
+ lines.push("", RECONNECT_HINT);
36312
+ }
36313
+ return lines.join("\n");
36314
+ }
36315
+ var RECONNECT_HINT = "Run `/mcp reconnect` to restart the servers that are not connected.";
36316
+ var RECONNECT_HINT_STATUSES = /* @__PURE__ */ new Set(["failed", "cancelled", "authenticationRequired", "notStarted"]);
36317
+ function serverDetail(server, startupError) {
36318
+ if (server.status === "authenticationRequired") {
36319
+ return NOT_SIGNED_IN;
36320
+ }
36321
+ const parts = [];
36322
+ const toolCount = server.toolCount ?? 0;
36323
+ if (server.status === "connected" || toolCount > 0) {
36324
+ parts.push(`${toolCount} ${toolCount === 1 ? "tool" : "tools"}`);
36325
+ }
36326
+ if (server.authStatus === "notLoggedIn" && server.status !== "disabled") {
36327
+ parts.push(NOT_SIGNED_IN);
36328
+ }
36329
+ const error62 = startupError ?? server.toolsError;
36330
+ const cleanedError = error62 === null ? "" : formatNoteDetail(error62);
36331
+ if (cleanedError.length > 0) {
36332
+ parts.push(cleanedError);
36333
+ }
36334
+ return parts.length === 0 ? null : parts.join("; ");
36335
+ }
36336
+ var NOT_SIGNED_IN = "not signed in";
36337
+ var MAX_ERROR_LENGTH = 160;
36338
+ var MAX_ERROR_SENTENCES = 2;
36339
+ var MAX_INPUT_LENGTH = 2e3;
36340
+ var WRAPPER_SEGMENT = /^(?:tool discovery failed|mcp startup failed|handshaking with mcp server failed|json-rpc error|startup error|error|mcp client for .+ failed to start|-32\d{3})$/i;
36341
+ var TYPE_PATH = /\S*\w::\w+::\S*/g;
36342
+ function cleanErrorMessage(text) {
36343
+ const oneLine = text.slice(0, MAX_INPUT_LENGTH).replace(/\s+/g, " ").trim();
36344
+ const message = oneLine.split(": ").map((segment) => segment.replace(TYPE_PATH, "").replace(/\s+/g, " ").trim()).filter((segment) => segment.length > 0 && !WRAPPER_SEGMENT.test(segment)).join(": ");
36345
+ if (message.length === 0) {
36346
+ return shorten(oneLine);
36347
+ }
36348
+ return shorten(message.split(/(?<=[.!?])\s+/).slice(0, MAX_ERROR_SENTENCES).join(" "));
36349
+ }
36350
+ function shorten(text) {
36351
+ const codePoints = Array.from(text);
36352
+ return codePoints.length > MAX_ERROR_LENGTH ? `${codePoints.slice(0, MAX_ERROR_LENGTH).join("").trimEnd()}\u2026` : text;
36353
+ }
36354
+ function formatNoteDetail(text) {
36355
+ return escapeMarkdown(cleanErrorMessage(text));
36356
+ }
36357
+ function escapeMarkdown(text) {
36358
+ return text.replace(/[\\`*_[\]<>~]/g, (character) => `\\${character}`);
36359
+ }
36360
+ var UNNAMED_SERVER = "(unnamed)";
36361
+ function inlineCode(name) {
36362
+ const text = name.replace(/\s+/g, " ");
36363
+ if (text.length === 0) {
36364
+ return UNNAMED_SERVER;
36365
+ }
36366
+ const longestRun = Math.max(0, ...Array.from(text.matchAll(/`+/g), (match) => match[0].length));
36367
+ const fence = "`".repeat(longestRun + 1);
36368
+ const padding = /^[` ]|[` ]$/.test(text) ? " " : "";
36369
+ return `${fence}${padding}${text}${padding}${fence}`;
36370
+ }
36371
+
36372
+ // src/mcp/McpCommand.ts
36373
+ var MCP_RECONNECT_STARTUP_TIMEOUT_MS = 3e4;
36374
+ var MCP_COMMAND_INPUT_HINT = "[reconnect]";
36375
+ var McpCommand = class {
36376
+ codexAcpClient;
36377
+ runWithProcessCheck;
36378
+ signIn;
36379
+ constructor(codexAcpClient, runWithProcessCheck, signIn) {
36380
+ this.codexAcpClient = codexAcpClient;
36381
+ this.runWithProcessCheck = runWithProcessCheck;
36382
+ this.signIn = signIn;
36383
+ }
36384
+ /** Returns the answer, or `null` when `signal` aborted. After an abort, the caller must send nothing. */
36385
+ async run(rest, sessionState, signal) {
36386
+ const args = rest.split(/\s+/).filter((arg) => arg.length > 0);
36387
+ if (args.length === 0) {
36388
+ const list = await this.listServers(sessionState);
36389
+ return signal?.aborted ? null : formatStatus(list.servers, /* @__PURE__ */ new Map());
36390
+ }
36391
+ if (args.length === 1 && args[0].toLowerCase() === "reconnect") {
36392
+ return await this.reconnect(sessionState, signal);
36393
+ }
36394
+ return `Command "/mcp" accepts no arguments, or \`reconnect\`.`;
36395
+ }
36396
+ async reconnect(sessionState, signal) {
36397
+ const before = await this.listServers(sessionState);
36398
+ if (before.servers.length === 0) {
36399
+ return NO_SERVERS_MESSAGE;
36400
+ }
36401
+ const notes = [];
36402
+ let reload = await this.reloadAndWait(sessionState, signal);
36403
+ if (reload.error !== null) {
36404
+ notes.push(`Could not reload the MCP configuration: ${formatNoteDetail(reload.error)}`);
36405
+ } else {
36406
+ notes.push(RELOAD_NOTE);
36407
+ const signIn = await this.signInServers(sessionState, reload, signal);
36408
+ notes.push(...signIn.notes);
36409
+ if (signIn.signedIn && !signal?.aborted) {
36410
+ const secondReload = await this.reloadAndWait(sessionState, signal);
36411
+ if (secondReload.error !== null) {
36412
+ notes.push(`Could not reload the MCP configuration after the sign-in: ${formatNoteDetail(secondReload.error)}`);
36413
+ reload = { ...secondReload, startup: reload.startup };
36414
+ } else {
36415
+ reload = secondReload;
36416
+ }
36417
+ }
36418
+ }
36419
+ if (signal?.aborted) {
36420
+ return null;
36421
+ }
36422
+ if (reload.startup === null) {
36423
+ notes.push("Some MCP servers are still starting. Run `/mcp` again later to see their status.");
36424
+ }
36425
+ return `${formatNotes(notes)}
36426
+
36427
+ ${formatStatus(reload.list.servers, startupErrors(reload.startup))}`;
36428
+ }
36429
+ /** Reloads the MCP configuration, and waits for the startup of the servers that the reloaded list contains. */
36430
+ async reloadAndWait(sessionState, signal) {
36431
+ const afterVersion = this.codexAcpClient.getMcpServerStartupVersion();
36432
+ try {
36433
+ await this.runWithProcessCheck(() => this.codexAcpClient.reloadMcpServers());
36434
+ } catch (err) {
36435
+ logger.error(`Failed to reload the MCP configuration for session ${sessionState.sessionId}`, err);
36436
+ return { error: errorMessage(err), startup: emptyStartupResult(), list: await this.listServers(sessionState) };
36437
+ }
36438
+ const reloaded = await this.listServers(sessionState);
36439
+ if (!reloaded.live) {
36440
+ return { error: null, startup: emptyStartupResult(), list: reloaded };
36441
+ }
36442
+ const serverNames = reloaded.servers.filter((server) => server.listedByCodex && server.status !== "disabled").map((server) => server.name);
36443
+ const startup = await this.waitForStartup(sessionState.sessionId, serverNames, afterVersion, signal);
36444
+ if (signal?.aborted) {
36445
+ return { error: null, startup, list: reloaded };
36446
+ }
36447
+ return { error: null, startup, list: await this.listServers(sessionState) };
36448
+ }
36449
+ /** Returns `null` at the timeout or when `signal` aborts. */
36450
+ async waitForStartup(threadId, serverNames, afterVersion, signal) {
36451
+ if (signal?.aborted) {
36452
+ return null;
36453
+ }
36454
+ const stop = new AbortController();
36455
+ const onAbort = () => stop.abort();
36456
+ signal?.addEventListener("abort", onAbort, { once: true });
36457
+ const timer = setTimeout(() => stop.abort(), MCP_RECONNECT_STARTUP_TIMEOUT_MS);
36458
+ try {
36459
+ return await this.runWithProcessCheck(() => this.codexAcpClient.awaitMcpServerStartup(
36460
+ serverNames,
36461
+ afterVersion,
36462
+ { threadId, signal: stop.signal }
36463
+ ));
36464
+ } catch (err) {
36465
+ if (stop.signal.aborted) {
36466
+ return null;
36467
+ }
36468
+ throw err;
36469
+ } finally {
36470
+ clearTimeout(timer);
36471
+ signal?.removeEventListener("abort", onAbort);
36472
+ }
36473
+ }
36474
+ async signInServers(sessionState, reload, signal) {
36475
+ const names = new Set(reload.list.servers.filter((server) => server.status === "authenticationRequired").map((server) => server.name));
36476
+ for (const failure3 of reload.startup?.failed ?? []) {
36477
+ if (failure3.failureReason === "reauthenticationRequired") {
36478
+ names.add(failure3.server);
36479
+ }
36480
+ }
36481
+ const notes = [];
36482
+ let signedIn = false;
36483
+ for (const name of names) {
36484
+ if (signal?.aborted) {
36485
+ break;
36486
+ }
36487
+ const label = inlineCode(name);
36488
+ switch (await this.signIn(sessionState.sessionId, name, signal)) {
36489
+ case "unsupported":
36490
+ notes.push(`MCP server ${label} needs authentication, but this client cannot open a sign-in page.`);
36491
+ break;
36492
+ case "signedIn":
36493
+ notes.push(`Signed in to MCP server ${label}.`);
36494
+ signedIn = true;
36495
+ break;
36496
+ case "failed":
36497
+ notes.push(`The sign-in to MCP server ${label} failed or was cancelled.`);
36498
+ break;
36499
+ }
36500
+ }
36501
+ return { notes, signedIn };
36502
+ }
36503
+ async listServers(sessionState) {
36504
+ const { servers, live } = await this.listServerStatus(sessionState.sessionId);
36505
+ const entries = servers.map(toEntry);
36506
+ for (const name of sessionState.sessionMcpServers ?? []) {
36507
+ if (!entries.some((entry) => entry.name === name)) {
36508
+ entries.push({ name, status: null, toolCount: null, toolsError: null, authStatus: null, listedByCodex: false });
36509
+ }
36510
+ }
36511
+ return { servers: entries, live };
36512
+ }
36513
+ async listServerStatus(threadId) {
36514
+ try {
36515
+ const servers = await this.runWithProcessCheck(() => listMcpServerStatus(this.codexAcpClient, threadId));
36516
+ return { servers, live: true };
36517
+ } catch (err) {
36518
+ if (!isThreadNotFoundError(err) && !isUnknownThreadError(err)) {
36519
+ throw err;
36520
+ }
36521
+ logger.log(`Codex does not know thread ${threadId}, so the MCP server list has no live status`);
36522
+ const servers = await this.runWithProcessCheck(() => listMcpServerStatus(this.codexAcpClient));
36523
+ return { servers, live: false };
36524
+ }
36525
+ }
36526
+ };
36527
+ var RELOAD_NOTE = "Reloaded the MCP configuration of all open sessions. Codex restarted the servers that failed, stopped, or changed. A connected server with unchanged settings keeps its connection.";
36528
+ function startupErrors(result) {
36529
+ return new Map((result?.failed ?? []).map((failure3) => [failure3.server, failure3.error]));
36530
+ }
36531
+ function emptyStartupResult() {
36532
+ return { ready: [], failed: [], cancelled: [] };
36533
+ }
36534
+ function errorMessage(err) {
36535
+ if (err instanceof Error) {
36536
+ return err.message;
36537
+ }
36538
+ return String(err);
36539
+ }
36540
+ function formatNotes(notes) {
36541
+ return ["**Reconnect:**", ...notes.map((note) => `- ${note}`)].join("\n");
36542
+ }
36543
+
35703
36544
  // src/CodexCommands.ts
35704
36545
  var GOAL_CONTINUATION_PROMPT = [{
35705
36546
  type: "text",
@@ -35710,14 +36551,15 @@ var CodexCommands = class {
35710
36551
  codexAcpClient;
35711
36552
  runWithProcessCheck;
35712
36553
  onLogout;
36554
+ mcpCommand;
35713
36555
  /** The commands that each session got last, to skip a publish that changes nothing. */
35714
36556
  published = /* @__PURE__ */ new WeakMap();
35715
- constructor(connection, codexAcpClient, runWithProcessCheck, onLogout = () => {
35716
- }) {
36557
+ constructor(connection, codexAcpClient, runWithProcessCheck, onLogout, mcpServerSignIn) {
35717
36558
  this.connection = connection;
35718
36559
  this.codexAcpClient = codexAcpClient;
35719
36560
  this.runWithProcessCheck = runWithProcessCheck;
35720
36561
  this.onLogout = onLogout;
36562
+ this.mcpCommand = new McpCommand(codexAcpClient, runWithProcessCheck, mcpServerSignIn);
35721
36563
  }
35722
36564
  /** Sends the available commands. With `onlyChanges`, it sends nothing when the commands did not change. */
35723
36565
  async publish(sessionState, shouldPublish = () => true, onlyChanges = false) {
@@ -35764,10 +36606,12 @@ var CodexCommands = class {
35764
36606
  const name = `$${skill.name}`;
35765
36607
  if (commands.has(name)) continue;
35766
36608
  const description = skill.shortDescription ?? skill.description ?? skill.name;
36609
+ const meta3 = airOnlyMeta(airClient, AIR_SKILL_PATH_KEY, skill.path);
35767
36610
  commands.set(name, {
35768
36611
  name,
35769
36612
  description,
35770
- input: null
36613
+ input: null,
36614
+ ...meta3 ? { _meta: meta3 } : {}
35771
36615
  });
35772
36616
  }
35773
36617
  }
@@ -35797,8 +36641,8 @@ var CodexCommands = class {
35797
36641
  },
35798
36642
  {
35799
36643
  name: "mcp",
35800
- description: "List configured Model Context Protocol (MCP) tools.",
35801
- input: null
36644
+ description: "Show the status of the MCP servers, or reconnect them.",
36645
+ input: { hint: MCP_COMMAND_INPUT_HINT }
35802
36646
  },
35803
36647
  {
35804
36648
  name: "skills",
@@ -35955,15 +36799,10 @@ var CodexCommands = class {
35955
36799
  return { handled: true };
35956
36800
  }
35957
36801
  case "mcp": {
35958
- const servers = await this.runWithProcessCheck(() => this.codexAcpClient.listMcpServers());
35959
- const configuredServers = servers.data.map((server) => {
35960
- const toolCount = Object.keys(server.tools ?? {}).length;
35961
- const resourceCount = (server.resources ?? []).length;
35962
- return `- ${server.name}: ${toolCount} tools, ${resourceCount} resources, auth=${server.authStatus}`;
35963
- });
35964
- const sessionServers = sessionState.sessionMcpServers ? sessionState.sessionMcpServers.map((serverName) => `- ${serverName}`) : [];
35965
- const lines = [...configuredServers, ...sessionServers];
35966
- const text = lines.length > 0 ? ["Configured MCP servers:", ...lines].join("\n") : "No MCP servers configured.";
36802
+ const text = await this.mcpCommand.run(command.rest, sessionState, options.signal);
36803
+ if (text === null) {
36804
+ return { handled: true };
36805
+ }
35967
36806
  const session = new ACPSessionConnection(this.connection, sessionId);
35968
36807
  await session.update(createAgentTextMessageChunk(text));
35969
36808
  return { handled: true };
@@ -36302,17 +37141,19 @@ var ToolCallReports = class {
36302
37141
  recordStart(key, update) {
36303
37142
  this.finishedToolCalls.delete(key);
36304
37143
  const fields = /* @__PURE__ */ new Map();
36305
- for (const [name, value] of reportedFields(update, this.compareMeta)) {
37144
+ const terminal = update.status === "completed" || update.status === "failed";
37145
+ const only = terminal ? FINISHED_FIELDS : void 0;
37146
+ for (const [name, value] of reportedFields(update, this.compareMeta && !terminal, only)) {
36306
37147
  fields.set(name, value);
36307
37148
  }
36308
37149
  this.openToolCalls.set(key, fields);
36309
37150
  return update;
36310
37151
  }
36311
37152
  recordUpdate(key, update) {
36312
- const finished = this.finishedToolCalls.get(key);
36313
- if (finished !== void 0) {
37153
+ const finished2 = this.finishedToolCalls.get(key);
37154
+ if (finished2 !== void 0) {
36314
37155
  const current = this.withoutLateOutput(update);
36315
- return current === null ? null : this.withoutUnchangedFields(current, finished, FINISHED_FIELDS);
37156
+ return current === null ? null : this.withoutUnchangedFields(current, finished2, FINISHED_FIELDS);
36316
37157
  }
36317
37158
  const fields = this.openToolCalls.get(key) ?? /* @__PURE__ */ new Map();
36318
37159
  this.openToolCalls.set(key, fields);
@@ -36322,7 +37163,7 @@ var ToolCallReports = class {
36322
37163
  withoutUnchangedFields(update, fields, recorded) {
36323
37164
  const prepared = { ...update };
36324
37165
  const meta3 = isRecord2(update._meta) ? { ...update._meta } : void 0;
36325
- for (const [name, value] of reportedFields(update, this.compareMeta)) {
37166
+ for (const [name, value] of reportedFields(update, this.compareMeta && recorded === void 0, recorded)) {
36326
37167
  if (fields.get(name) === value) {
36327
37168
  if (name.startsWith(META_FIELD_PREFIX)) {
36328
37169
  delete meta3?.[name.slice(META_FIELD_PREFIX.length)];
@@ -36374,10 +37215,11 @@ var ToolCallReports = class {
36374
37215
  }
36375
37216
  }
36376
37217
  };
36377
- function reportedFields(update, withMeta) {
37218
+ function reportedFields(update, withMeta, only) {
36378
37219
  const fields = [];
36379
37220
  const record2 = update;
36380
37221
  for (const name of COMPARED_FIELDS) {
37222
+ if (only !== void 0 && !only.has(name)) continue;
36381
37223
  const value = record2[name];
36382
37224
  if (value !== void 0) fields.push([name, JSON.stringify(value)]);
36383
37225
  }
@@ -36573,6 +37415,26 @@ function fromAccountUpdated(notification, previous) {
36573
37415
  return gatewayStatus();
36574
37416
  }
36575
37417
  }
37418
+ function accountFromUpdated(notification, previous) {
37419
+ switch (notification.authMode) {
37420
+ case null:
37421
+ return null;
37422
+ case "chatgpt":
37423
+ case "chatgptAuthTokens": {
37424
+ const previousChatGpt = previous?.type === "chatgpt" ? previous : null;
37425
+ return {
37426
+ type: "chatgpt",
37427
+ email: previousChatGpt?.email ?? null,
37428
+ planType: notification.planType ?? previousChatGpt?.planType ?? "unknown"
37429
+ };
37430
+ }
37431
+ case "apikey":
37432
+ case "personalAccessToken":
37433
+ return { type: "apiKey" };
37434
+ default:
37435
+ return previous;
37436
+ }
37437
+ }
36576
37438
  function sameAuthStatus(previous, next) {
36577
37439
  if (!previous) {
36578
37440
  return false;
@@ -36641,34 +37503,6 @@ var ClientCapabilities = class _ClientCapabilities {
36641
37503
  }
36642
37504
  };
36643
37505
 
36644
- // src/tool-calls/reporters/McpStartupReporter.ts
36645
- import { randomUUID } from "node:crypto";
36646
- var McpStartupReporter = class {
36647
- static failures(event) {
36648
- return [
36649
- ...event.failed.map((server) => failure2(
36650
- server.server,
36651
- `[codex-acp forwarded startup error] MCP server \`${server.server}\` failed to start: ${server.error}`
36652
- )),
36653
- ...event.cancelled.map((server) => failure2(
36654
- server,
36655
- `[codex-acp forwarded startup error] MCP server \`${server}\` startup was cancelled.`
36656
- ))
36657
- ];
36658
- }
36659
- };
36660
- function failure2(serverName, message) {
36661
- return {
36662
- // A unique id, so that a later report for the same server cannot replace this one.
36663
- toolCallId: `mcp_startup.${encodeURIComponent(serverName)}.${randomUUID()}`,
36664
- report: "start",
36665
- kind: "other",
36666
- title: `mcp__${serverName}__startup`,
36667
- status: "failed",
36668
- result: [textContent(message)]
36669
- };
36670
- }
36671
-
36672
37506
  // src/tool-calls/reporters/PlanReviewReporter.ts
36673
37507
  var IMPLEMENT_PLAN_OPTION_ID = "implement_plan";
36674
37508
  var REVISE_PLAN_OPTION_ID = "revise_plan";
@@ -36778,7 +37612,7 @@ function clientSupportsSubagents(capabilities) {
36778
37612
  }
36779
37613
 
36780
37614
  // src/CodexAcpServer.ts
36781
- import { randomUUID as randomUUID2 } from "node:crypto";
37615
+ import { randomUUID as randomUUID3 } from "node:crypto";
36782
37616
 
36783
37617
  // src/TitleGenerator.ts
36784
37618
  var TITLE_MODEL = "gpt-5.6-luna";
@@ -37337,15 +38171,20 @@ var CodexSessionCompactions = class {
37337
38171
  var CODEX_PROCESS_EXITED_ERROR_CODE = 1001;
37338
38172
  var TITLE_GENERATION_SETTLE_TIMEOUT_MS = 1e4;
37339
38173
  var NO_ACTIVE_TURN_RETRY_DELAYS_MS = [25, 50, 100, 200, 400];
38174
+ var ACCOUNT_READ_LOAD_GRACE_MS = 1e3;
37340
38175
  function clientSupportsTypedSessionFailures(capabilities) {
37341
38176
  return clientSupportsAirCapability(capabilities, AIR_SESSION_FAILURE_KEY);
37342
38177
  }
37343
38178
  function clientSupportsAgentFileChangeReports(capabilities) {
37344
38179
  return clientSupportsAirCapability(capabilities, AIR_AGENT_FILE_CHANGE_REPORT_KEY);
37345
38180
  }
38181
+ function isAuthRequiredError(error62) {
38182
+ return error62 instanceof RequestError && error62.code === RequestError.authRequired().code;
38183
+ }
37346
38184
  var CodexAcpServer = class {
37347
38185
  codexAcpClient;
37348
38186
  connection;
38187
+ mcpServerSignIn;
37349
38188
  reportingConnection;
37350
38189
  defaultAuthRequest;
37351
38190
  getExitCode;
@@ -37359,8 +38198,18 @@ var CodexAcpServer = class {
37359
38198
  booleanConfigOptionsSupported;
37360
38199
  /** Last `authStatus` pushed to the client; used to suppress duplicates. */
37361
38200
  currentAuthStatus;
38201
+ /**
38202
+ * The last known account of the agent. Every successful account read and every `account/updated` sets it,
38203
+ * and a logout clears it. A session open takes it when the account read is unavailable.
38204
+ */
38205
+ lastReadAccount = null;
38206
+ /**
38207
+ * Counts the calls of {@link setAuthStatus}, also the calls that push nothing. A late account read
38208
+ * compares it with the value at the start of the read: a change means a newer status.
38209
+ */
38210
+ authStatusVersion = 0;
37362
38211
  sessions;
37363
- pendingMcpStartupSessions;
38212
+ mcpSessionStartup;
37364
38213
  pendingTurnStarts;
37365
38214
  activePrompts;
37366
38215
  steeringQueues;
@@ -37375,7 +38224,6 @@ var CodexAcpServer = class {
37375
38224
  providerUpdate = null;
37376
38225
  constructor(connection, codexAcpClient, defaultAuthRequest, getExitCode, getRecentStderr, codexProcessState) {
37377
38226
  this.sessions = /* @__PURE__ */ new Map();
37378
- this.pendingMcpStartupSessions = /* @__PURE__ */ new Map();
37379
38227
  this.pendingTurnStarts = /* @__PURE__ */ new Map();
37380
38228
  this.activePrompts = /* @__PURE__ */ new Map();
37381
38229
  this.steeringQueues = /* @__PURE__ */ new Map();
@@ -37387,12 +38235,25 @@ var CodexAcpServer = class {
37387
38235
  this.reportingConnection = new ToolCallReportingConnection(connection);
37388
38236
  this.connection = this.reportingConnection.asClientConnection();
37389
38237
  this.codexAcpClient = codexAcpClient;
38238
+ this.mcpServerSignIn = createMcpServerSignIn(
38239
+ this.connection,
38240
+ () => this.codexAcpClient,
38241
+ () => this.clientCapabilities
38242
+ );
38243
+ this.mcpSessionStartup = new McpSessionStartup(
38244
+ this.connection,
38245
+ () => this.codexAcpClient,
38246
+ (operation) => this.runWithProcessCheck(operation),
38247
+ this.mcpServerSignIn,
38248
+ () => this.capabilities,
38249
+ (sessionId) => this.sessions.has(sessionId) && !this.sessionIsClosing(sessionId)
38250
+ );
37390
38251
  this.defaultAuthRequest = defaultAuthRequest ?? null;
37391
38252
  this.codexProcessState = codexProcessState ?? null;
37392
38253
  this.captureStderr();
37393
38254
  this.getExitCode = getExitCode ?? (() => this.codexProcessState?.connection.process.exitCode ?? null);
37394
38255
  this.getRecentStderr = getRecentStderr ?? (() => this.codexProcessState?.stderr ?? "");
37395
- this.sessionFailureEpoch = randomUUID2();
38256
+ this.sessionFailureEpoch = randomUUID3();
37396
38257
  this.clientInfo = null;
37397
38258
  this.clientCapabilities = null;
37398
38259
  this.capabilities = ClientCapabilities.DEFAULT;
@@ -37406,7 +38267,8 @@ var CodexAcpServer = class {
37406
38267
  this.connection,
37407
38268
  client,
37408
38269
  (operation) => this.runWithProcessCheck(operation),
37409
- () => this.refreshAuthState(null)
38270
+ () => this.refreshAuthState(null),
38271
+ this.mcpServerSignIn
37410
38272
  );
37411
38273
  }
37412
38274
  async initialize(_params) {
@@ -37496,7 +38358,7 @@ var CodexAcpServer = class {
37496
38358
  }
37497
38359
  switch (methodRequest.method) {
37498
38360
  case "authentication/status":
37499
- return await this.runWithProcessCheck(() => this.codexAcpClient.getAuthenticationStatus());
38361
+ return await this.readAuthenticationStatus();
37500
38362
  case "authentication/logout": {
37501
38363
  await this.logout({});
37502
38364
  return {};
@@ -37579,10 +38441,42 @@ var CodexAcpServer = class {
37579
38441
  }
37580
38442
  }
37581
38443
  }
38444
+ /**
38445
+ * Answers `authentication/status`.
38446
+ *
38447
+ * An unavailable account read still tells that the agent has a ChatGPT login, because only such a login
38448
+ * runs the routing discovery, see {@link isAccountReadUnavailableError}. The answer is then `chat-gpt` with
38449
+ * the email of the last known account, or an empty email. A read that failed because the login does not
38450
+ * work answers `unauthenticated`, see {@link isAccountReadAuthFailureError}. Another error rejects.
38451
+ */
38452
+ async readAuthenticationStatus() {
38453
+ try {
38454
+ return await this.runWithProcessCheck(() => this.codexAcpClient.getAuthenticationStatus());
38455
+ } catch (error62) {
38456
+ if (isAccountReadUnavailableError(error62)) {
38457
+ logger.log("Account read unavailable, so the authentication status uses the last known account", { error: String(error62) });
38458
+ const account = this.lastReadAccount;
38459
+ return { type: "chat-gpt", email: account?.type === "chatgpt" ? account.email ?? "" : "" };
38460
+ }
38461
+ if (isAccountReadAuthFailureError(error62)) {
38462
+ logger.log("The account read found that the agent needs a login", { error: String(error62) });
38463
+ return { type: "unauthenticated" };
38464
+ }
38465
+ throw error62;
38466
+ }
38467
+ }
38468
+ /**
38469
+ * Logs in with the default auth request, or throws `auth_required`, when the agent needs a login.
38470
+ * Returns the account read that found no login needed, or `null` when there was no read or a login ran.
38471
+ * Returns `"unavailable"` when the read failed without an answer about the login, see
38472
+ * {@link isAccountReadUnavailableError}. The session open then continues: a missing login fails the
38473
+ * resume or the first turn with the real error.
38474
+ */
37582
38475
  async checkAuthorization() {
37583
- const authNeeded = await this.runWithProcessCheck(() => this.codexAcpClient.authRequired());
37584
- logger.log("Auth requirement checked", { authRequired: authNeeded });
37585
- if (authNeeded) {
38476
+ const requirement = await this.readAccountOrUnavailable(() => this.codexAcpClient.readAuthRequirement());
38477
+ if (requirement === "unavailable") return "unavailable";
38478
+ logger.log("Auth requirement checked", { authRequired: requirement.required });
38479
+ if (requirement.required) {
37586
38480
  if (this.defaultAuthRequest) {
37587
38481
  logger.log("Authenticating with default auth request...", {
37588
38482
  authRequest: this.defaultAuthRequest
@@ -37593,7 +38487,9 @@ var CodexAcpServer = class {
37593
38487
  logger.log("Authentication required but no default auth request provided, return to IDE");
37594
38488
  throw RequestError.authRequired();
37595
38489
  }
38490
+ return null;
37596
38491
  }
38492
+ return requirement.account;
37597
38493
  }
37598
38494
  async getOrCreateSession(request) {
37599
38495
  try {
@@ -37681,7 +38577,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
37681
38577
  async tryCreateSession(request, operation = "sessionId" in request ? "resume" : "new") {
37682
38578
  const existingSessionRequest = request;
37683
38579
  const requestedSessionGeneration = operation === "resume" ? this.beginSessionOpen(existingSessionRequest.sessionId) : null;
37684
- await this.checkAuthorization();
38580
+ const knownAccount = await this.checkAuthorization();
37685
38581
  const requestedMcpServers = request.mcpServers ?? [];
37686
38582
  const mcpServerStartupVersion = requestedMcpServers.length > 0 ? this.codexAcpClient.getMcpServerStartupVersion() : null;
37687
38583
  let sessionMetadata;
@@ -37713,7 +38609,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
37713
38609
  const authProvider = sessionMetadata.modelProvider ?? this.codexAcpClient.getModelProvider();
37714
38610
  let authState;
37715
38611
  try {
37716
- authState = await this.getAuthStateForProvider(authProvider);
38612
+ authState = await this.getAuthStateForProvider(authProvider, knownAccount);
37717
38613
  } catch (err) {
37718
38614
  if (resumeSubscribed && requestedSessionGeneration !== null) {
37719
38615
  await this.cleanupStaleSessionOpen(sessionId, requestedSessionGeneration);
@@ -37774,30 +38670,21 @@ Check ${configPath} and project .codex directories, especially their config.toml
37774
38670
  resumeSubscribed = false;
37775
38671
  const canPublishSessionUpdates = operation !== "fork";
37776
38672
  if (requestedMcpServers.length > 0 && mcpServerStartupVersion !== null) {
37777
- const pendingStartup = this.createPendingMcpStartupSession(
37778
- requestedMcpServers,
37779
- mcpServerStartupVersion
37780
- );
37781
- if (canPublishSessionUpdates) {
37782
- this.pendingMcpStartupSessions.set(sessionId, pendingStartup);
37783
- }
37784
- const startupAwaitTimeoutMs = parseMcpStartupAwaitTimeoutMs(request._meta);
37785
- if (startupAwaitTimeoutMs !== void 0 && startupAwaitTimeoutMs > 0) {
38673
+ const startupWait = this.mcpSessionStartup.begin(sessionId, requestedMcpServers, mcpServerStartupVersion, {
38674
+ publish: canPublishSessionUpdates,
38675
+ awaitTimeoutMs: parseMcpStartupAwaitTimeoutMs(request._meta),
38676
+ skippedServers: sessionMetadata.skippedMcpServers
38677
+ });
38678
+ if (startupWait !== null) {
37786
38679
  try {
37787
- await raceMcpStartupTimeout(pendingStartup.startup, startupAwaitTimeoutMs);
38680
+ await startupWait;
37788
38681
  } catch (err) {
37789
- if (this.pendingMcpStartupSessions.get(sessionId) === pendingStartup) {
37790
- this.pendingMcpStartupSessions.delete(sessionId);
37791
- }
37792
38682
  await this.closeSession({ sessionId }).catch((closeError) => {
37793
38683
  logger.error(`Failed to close session ${sessionId} after a failed MCP startup wait`, closeError);
37794
38684
  });
37795
38685
  throw err;
37796
38686
  }
37797
38687
  }
37798
- if (canPublishSessionUpdates) {
37799
- this.publishMcpStartupStatusAsync(sessionId);
37800
- }
37801
38688
  }
37802
38689
  if (canPublishSessionUpdates) {
37803
38690
  this.publishAvailableCommandsAsync(sessionState, sessionGeneration);
@@ -37810,7 +38697,15 @@ Check ${configPath} and project .codex directories, especially their config.toml
37810
38697
  const sessionModeState = sessionState.agentMode.toSessionModeState(sessionState.clientCapabilities.airClient);
37811
38698
  return [sessionId, sessionModelState, sessionModeState];
37812
38699
  }
37813
- async getAuthStateForProvider(authProvider) {
38700
+ /**
38701
+ * Reads the account of `authProvider` and pushes the auth status. `knownAccount` is an account read that
38702
+ * the caller already has.
38703
+ *
38704
+ * When the read is unavailable, the adapter pushes no status, so the client keeps the last status.
38705
+ * A `none` status would be false here: AIR shows a login request for it. The session keeps the
38706
+ * account of the last read.
38707
+ */
38708
+ async getAuthStateForProvider(authProvider, knownAccount = null) {
37814
38709
  if (!this.authProviderUsesOpenAiAccount(authProvider)) {
37815
38710
  await this.publishAuthStatus(authProvider, null);
37816
38711
  return {
@@ -37818,13 +38713,96 @@ Check ${configPath} and project .codex directories, especially their config.toml
37818
38713
  authConfigured: true
37819
38714
  };
37820
38715
  }
37821
- const accountResponse = await this.runWithProcessCheck(() => this.codexAcpClient.getAccount());
38716
+ const accountResponse = knownAccount === "unavailable" ? null : knownAccount ?? await this.readAccountIfAvailable();
38717
+ if (accountResponse === null) {
38718
+ logger.log("No account read, so the auth status stays as it was");
38719
+ return {
38720
+ account: this.lastReadAccount,
38721
+ authConfigured: true
38722
+ };
38723
+ }
38724
+ this.lastReadAccount = accountResponse.account;
37822
38725
  await this.publishAuthStatus(authProvider, accountResponse.account);
37823
38726
  return {
37824
38727
  account: accountResponse.account,
37825
38728
  authConfigured: accountResponse.account !== null || !accountResponse.requiresOpenaiAuth
37826
38729
  };
37827
38730
  }
38731
+ /** Reads the account, or returns `null` when the read failed without an answer about the login. */
38732
+ async readAccountIfAvailable() {
38733
+ const response = await this.readAccountOrUnavailable(() => this.codexAcpClient.getAccount());
38734
+ return response === "unavailable" ? null : response;
38735
+ }
38736
+ /**
38737
+ * Runs an account read, or returns `"unavailable"` when it failed without an answer about the login,
38738
+ * see {@link isAccountReadUnavailableError}. Another error rejects.
38739
+ */
38740
+ async readAccountOrUnavailable(read) {
38741
+ try {
38742
+ return await this.runWithProcessCheck(read);
38743
+ } catch (error62) {
38744
+ if (!isAccountReadUnavailableError(error62)) throw error62;
38745
+ logger.log("Account read unavailable, the login state is unknown", { error: String(error62) });
38746
+ return "unavailable";
38747
+ }
38748
+ }
38749
+ /**
38750
+ * Applies the account read of a load that answered before the read ended.
38751
+ *
38752
+ * The answer applies only while the load is the current open of the session, and while no auth status
38753
+ * was set after the read started. A close or a new load of the session makes the answer old.
38754
+ * A newer status, from any source, also makes it old. An old answer changes nothing.
38755
+ *
38756
+ * - A read that found no login needed sets the account of the session and pushes the auth status.
38757
+ * - A read that found that the agent needs a login (`auth_required`), or that failed because the login
38758
+ * does not work ({@link isAccountReadAuthFailureError}, for example a revoked token), clears the account
38759
+ * and pushes the `none` status at once, with no second read. The client then shows a login request,
38760
+ * as it does for an `auth_required` error of the load.
38761
+ * - A read that failed because another client logged in or out reads the account again and pushes it.
38762
+ * - An unavailable read changes nothing.
38763
+ * - Any other failure is logged as an error and pushes nothing. It tells nothing sure about the login,
38764
+ * so the client keeps the last status.
38765
+ */
38766
+ applyLateAccountRead(sessionState, authorization, authStatusVersionAtStart) {
38767
+ const sessionIsCurrent = () => this.sessions.get(sessionState.sessionId) === sessionState;
38768
+ const answerIsCurrent = () => {
38769
+ if (!sessionIsCurrent()) {
38770
+ logger.log("A late account read ended after the load closed, so it changes nothing", { sessionId: sessionState.sessionId });
38771
+ return false;
38772
+ }
38773
+ if (this.authStatusVersion !== authStatusVersionAtStart) {
38774
+ logger.log("A late account read ended after a newer auth status, so it changes nothing", { sessionId: sessionState.sessionId });
38775
+ return false;
38776
+ }
38777
+ return true;
38778
+ };
38779
+ void authorization.then(
38780
+ async (knownAccount) => {
38781
+ if (knownAccount === "unavailable" || !answerIsCurrent()) return;
38782
+ const authState = await this.getAuthStateForProvider(sessionState.authProvider, knownAccount);
38783
+ if (!sessionIsCurrent()) return;
38784
+ sessionState.account = authState.account;
38785
+ sessionState.authConfigured = authState.authConfigured;
38786
+ },
38787
+ async (error62) => {
38788
+ if (!answerIsCurrent()) return;
38789
+ if (isAuthRequiredError(error62) || isAccountReadAuthFailureError(error62)) {
38790
+ logger.log("A late account read found that the agent needs a login", { error: String(error62) });
38791
+ this.lastReadAccount = null;
38792
+ sessionState.account = null;
38793
+ sessionState.authConfigured = false;
38794
+ await this.publishAuthStatus(sessionState.authProvider, null);
38795
+ return;
38796
+ }
38797
+ if (isAccountReadAccountChangedError(error62)) {
38798
+ logger.log("The account changed while a late account read ran, so the adapter reads it again");
38799
+ await this.publishAuthStatusRead();
38800
+ return;
38801
+ }
38802
+ logger.error("A late account read failed, so the auth status stays as it was", error62);
38803
+ }
38804
+ ).catch((error62) => logger.error("Failed to apply a late account read", error62));
38805
+ }
37828
38806
  authProviderUsesOpenAiAccount(authProvider) {
37829
38807
  return authProvider === null || authProvider === "openai";
37830
38808
  }
@@ -37965,7 +38943,8 @@ Check ${configPath} and project .codex directories, especially their config.toml
37965
38943
  } finally {
37966
38944
  if (this.getSessionGeneration(params.sessionId) === closeGeneration) {
37967
38945
  this.sessions.delete(params.sessionId);
37968
- this.pendingMcpStartupSessions.delete(params.sessionId);
38946
+ this.mcpSessionStartup.close(params.sessionId);
38947
+ this.codexAcpClient.appServerClient.mcpStartup.forgetThread(params.sessionId);
37969
38948
  this.pendingTurnStarts.delete(params.sessionId);
37970
38949
  this.activePrompts.delete(params.sessionId);
37971
38950
  this.steeringQueues.delete(params.sessionId);
@@ -37994,7 +38973,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
37994
38973
  return {};
37995
38974
  }
37996
38975
  hasLocalSession(sessionId) {
37997
- return this.sessions.has(sessionId) || this.pendingMcpStartupSessions.has(sessionId) || this.pendingTurnStarts.has(sessionId) || this.activePrompts.has(sessionId) || this.hasPendingSessionOpen(sessionId) || this.sessionIsClosing(sessionId);
38976
+ return this.sessions.has(sessionId) || this.mcpSessionStartup.isPending(sessionId) || this.pendingTurnStarts.has(sessionId) || this.activePrompts.has(sessionId) || this.hasPendingSessionOpen(sessionId) || this.sessionIsClosing(sessionId);
37998
38977
  }
37999
38978
  hasPendingSessionOpen(sessionId) {
38000
38979
  return this.sessionOpenGenerations.get(sessionId) === this.getSessionGeneration(sessionId);
@@ -38223,12 +39202,22 @@ Check ${configPath} and project .codex directories, especially their config.toml
38223
39202
  * Never rejects: an unreadable source means "nothing to report", not an
38224
39203
  * error. The client then keeps showing the last pushed value, or "not
38225
39204
  * reported" when there was none.
39205
+ *
39206
+ * A read that failed because the login does not work, for example a revoked
39207
+ * token, is an answer: it pushes the `none` status, see
39208
+ * {@link isAccountReadAuthFailureError}.
38226
39209
  */
38227
39210
  async publishAuthStatusRead() {
38228
39211
  let authStatus;
38229
39212
  try {
38230
39213
  authStatus = await this.readAgentAuthIdentity();
38231
39214
  } catch (error62) {
39215
+ if (isAccountReadAuthFailureError(error62)) {
39216
+ logger.log("The account read found that the agent needs a login", { error: String(error62) });
39217
+ this.lastReadAccount = null;
39218
+ await this.setAuthStatus(fromAccount(null));
39219
+ return;
39220
+ }
38232
39221
  logger.log("Cannot determine auth status", { error: String(error62) });
38233
39222
  return;
38234
39223
  }
@@ -38251,6 +39240,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
38251
39240
  return gatewayStatus(modelProvider);
38252
39241
  }
38253
39242
  const accountResponse = await this.runWithProcessCheck(() => this.codexAcpClient.getAccount());
39243
+ this.lastReadAccount = accountResponse.account;
38254
39244
  return fromAccount(accountResponse.account);
38255
39245
  }
38256
39246
  /**
@@ -38287,6 +39277,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
38287
39277
  * invalidate; only a gateway logout or a provider change does.
38288
39278
  */
38289
39279
  async applyAccountUpdated(notification) {
39280
+ this.lastReadAccount = accountFromUpdated(notification, this.lastReadAccount);
38290
39281
  try {
38291
39282
  if (this.codexAcpClient.getAuthGatewayProviderName() !== null) {
38292
39283
  return;
@@ -38316,6 +39307,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
38316
39307
  * so no payload can equal it.
38317
39308
  */
38318
39309
  async setAuthStatus(next) {
39310
+ this.authStatusVersion += 1;
38319
39311
  if (sameAuthStatus(this.currentAuthStatus, next)) {
38320
39312
  return;
38321
39313
  }
@@ -38783,18 +39775,30 @@ Check ${configPath} and project .codex directories, especially their config.toml
38783
39775
  }
38784
39776
  async getOrCreateSessionWithHistory(request) {
38785
39777
  const requestedSessionGeneration = this.beginSessionOpen(request.sessionId);
38786
- await this.checkAuthorization();
39778
+ const loadStartedAt = performance.now();
39779
+ const graceLeftMs = () => ACCOUNT_READ_LOAD_GRACE_MS - (performance.now() - loadStartedAt);
39780
+ const authStatusVersionAtStart = this.authStatusVersion;
39781
+ const authorization = this.defaultAuthRequest ? Promise.resolve(await this.checkAuthorization()) : this.checkAuthorization();
39782
+ authorization.catch(() => {
39783
+ });
38787
39784
  const requestedMcpServers = request.mcpServers ?? [];
38788
39785
  const mcpServerStartupVersion = requestedMcpServers.length > 0 ? this.codexAcpClient.getMcpServerStartupVersion() : null;
38789
39786
  logger.log(`Load existing session: ${request.sessionId}...`);
38790
39787
  let subscribed = false;
38791
39788
  let sessionMetadata;
39789
+ let knownAccount;
38792
39790
  try {
38793
- sessionMetadata = await this.runWithProcessCheck(
38794
- () => this.codexAcpClient.loadSession(request, () => {
38795
- subscribed = true;
38796
- })
38797
- );
39791
+ try {
39792
+ sessionMetadata = await this.runWithProcessCheck(
39793
+ () => this.codexAcpClient.loadSession(request, () => {
39794
+ subscribed = true;
39795
+ })
39796
+ );
39797
+ } catch (err) {
39798
+ await settledWithin(authorization, graceLeftMs());
39799
+ throw err;
39800
+ }
39801
+ knownAccount = await settledWithin(authorization, graceLeftMs());
38798
39802
  } catch (err) {
38799
39803
  if (subscribed) {
38800
39804
  await this.cleanupStaleSessionOpen(request.sessionId, requestedSessionGeneration);
@@ -38805,7 +39809,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
38805
39809
  const authProvider = sessionMetadata.modelProvider ?? this.codexAcpClient.getModelProvider();
38806
39810
  let authState;
38807
39811
  try {
38808
- authState = await this.getAuthStateForProvider(authProvider);
39812
+ authState = await this.getAuthStateForProvider(authProvider, knownAccount === "pending" ? "unavailable" : knownAccount);
38809
39813
  } catch (err) {
38810
39814
  if (subscribed) {
38811
39815
  await this.cleanupStaleSessionOpen(request.sessionId, requestedSessionGeneration);
@@ -38862,13 +39866,15 @@ Check ${configPath} and project .codex directories, especially their config.toml
38862
39866
  () => sessionState.sessionTitleSource
38863
39867
  );
38864
39868
  this.installSessionState(sessionState);
39869
+ if (knownAccount === "pending") {
39870
+ this.applyLateAccountRead(sessionState, authorization, authStatusVersionAtStart);
39871
+ }
38865
39872
  subscribed = false;
38866
39873
  if (requestedMcpServers.length > 0 && mcpServerStartupVersion !== null) {
38867
- this.pendingMcpStartupSessions.set(
38868
- sessionId,
38869
- this.createPendingMcpStartupSession(requestedMcpServers, mcpServerStartupVersion)
38870
- );
38871
- this.publishMcpStartupStatusAsync(sessionId);
39874
+ this.mcpSessionStartup.begin(sessionId, requestedMcpServers, mcpServerStartupVersion, {
39875
+ publish: true,
39876
+ skippedServers: sessionMetadata.skippedMcpServers
39877
+ });
38872
39878
  }
38873
39879
  await this.publishAvailableCommands(sessionState, requestedSessionGeneration);
38874
39880
  await this.publishCurrentGoalBestEffort(sessionState, requestedSessionGeneration, true);
@@ -39017,7 +40023,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
39017
40023
  }
39018
40024
  }
39019
40025
  async publishThreadHistoryTitle(session, sessionState, thread, firstItems) {
39020
- const explicitTitle = this.normalizeSessionTitle(thread.name);
40026
+ const explicitTitle = normalizeSessionTitle(thread.name);
39021
40027
  if (explicitTitle) {
39022
40028
  sessionState.sessionTitle = explicitTitle;
39023
40029
  sessionState.sessionTitleSource = "explicit";
@@ -39028,13 +40034,13 @@ Check ${configPath} and project .codex directories, especially their config.toml
39028
40034
  });
39029
40035
  return;
39030
40036
  }
39031
- const historyTitle = this.findFirstUserMessageTitle(firstItems) ?? this.normalizeSessionTitle(thread.preview);
40037
+ const historyTitle = this.findFirstUserMessageTitle(firstItems) ?? normalizeSessionTitle(thread.preview);
39032
40038
  await this.publishFallbackSessionTitle(sessionState, historyTitle);
39033
40039
  }
39034
40040
  findFirstUserMessageTitle(items) {
39035
40041
  for (const item of items) {
39036
40042
  if (item.type !== "userMessage") continue;
39037
- const title = this.normalizeSessionTitle(item.content.filter((input2) => input2.type === "text").map((input2) => input2.text).join(" "));
40043
+ const title = normalizeSessionTitle(item.content.filter((input2) => input2.type === "text").map((input2) => input2.text).join(" "));
39038
40044
  if (title) return title;
39039
40045
  }
39040
40046
  return null;
@@ -39081,11 +40087,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
39081
40087
  }
39082
40088
  }
39083
40089
  createPromptFallbackTitle(prompt) {
39084
- return this.normalizeSessionTitle(prompt.filter((block) => block.type === "text").map((block) => block.text).join(" "));
39085
- }
39086
- normalizeSessionTitle(title) {
39087
- const normalized = title?.replace(/\s+/g, " ").trim() ?? "";
39088
- return normalized.length > 0 ? normalized : null;
40090
+ return normalizeSessionTitle(prompt.filter((block) => block.type === "text").map((block) => block.text).join(" "));
39089
40091
  }
39090
40092
  async createHistoryUpdates(item, sessionState) {
39091
40093
  const renderer = new AcpToolCallRenderer(sessionState.clientCapabilities);
@@ -39240,102 +40242,6 @@ Check ${configPath} and project .codex directories, especially their config.toml
39240
40242
  logger.log("Skipping MCP server recovery for load/resume without explicit mcpServers");
39241
40243
  return [];
39242
40244
  }
39243
- publishMcpStartupStatusAsync(sessionId) {
39244
- void this.doPublishMcpStartupStatus(sessionId);
39245
- }
39246
- createPendingMcpStartupSession(mcpServers, afterVersion) {
39247
- const requestedServers = new Set(getRequestedMcpServerNames(mcpServers));
39248
- return {
39249
- requestedServers,
39250
- startup: this.runWithProcessCheck(
39251
- () => this.codexAcpClient.awaitMcpServerStartup(Array.from(requestedServers), afterVersion)
39252
- )
39253
- };
39254
- }
39255
- async doPublishMcpStartupStatus(sessionId) {
39256
- const pendingStartup = this.pendingMcpStartupSessions.get(sessionId);
39257
- if (!pendingStartup) {
39258
- return;
39259
- }
39260
- try {
39261
- const mcpStartup = await pendingStartup.startup;
39262
- if (!this.sessions.has(sessionId) || this.sessionIsClosing(sessionId) || this.pendingMcpStartupSessions.get(sessionId) !== pendingStartup) {
39263
- return;
39264
- }
39265
- await this.publishMcpStartupStatus(sessionId, mcpStartup, pendingStartup.requestedServers);
39266
- } catch (err) {
39267
- logger.error(`Failed to publish MCP startup status for session ${sessionId}`, err);
39268
- } finally {
39269
- if (this.pendingMcpStartupSessions.get(sessionId) === pendingStartup) {
39270
- this.pendingMcpStartupSessions.delete(sessionId);
39271
- }
39272
- }
39273
- }
39274
- async publishMcpStartupStatus(sessionId, mcpStartup, requestedServers) {
39275
- const filteredStartup = requestedServers ? {
39276
- ready: mcpStartup.ready.filter((server) => requestedServers.has(server)),
39277
- failed: mcpStartup.failed.filter((server) => requestedServers.has(server.server)),
39278
- cancelled: mcpStartup.cancelled.filter((server) => requestedServers.has(server))
39279
- } : mcpStartup;
39280
- const failuresAfterOauth = [];
39281
- const readyAfterOauth = [...filteredStartup.ready];
39282
- for (const failure3 of filteredStartup.failed) {
39283
- if (failure3.failureReason !== "reauthenticationRequired" || !clientSupportsUrlElicitation(this.clientCapabilities)) {
39284
- failuresAfterOauth.push(failure3);
39285
- continue;
39286
- }
39287
- try {
39288
- const authenticated = await this.authenticateMcpServer(sessionId, failure3.server);
39289
- if (authenticated) {
39290
- readyAfterOauth.push(failure3.server);
39291
- } else {
39292
- failuresAfterOauth.push(failure3);
39293
- }
39294
- } catch (error62) {
39295
- logger.error(`Failed to authenticate MCP server ${failure3.server}`, error62);
39296
- failuresAfterOauth.push(failure3);
39297
- }
39298
- }
39299
- const renderer = new AcpToolCallRenderer(this.capabilities);
39300
- for (const facts4 of McpStartupReporter.failures({
39301
- ...filteredStartup,
39302
- ready: readyAfterOauth,
39303
- failed: failuresAfterOauth
39304
- })) {
39305
- await this.connection.notify(methods.client.session.update, {
39306
- sessionId,
39307
- update: renderer.render(facts4)
39308
- });
39309
- }
39310
- }
39311
- async authenticateMcpServer(sessionId, serverName) {
39312
- const elicitationId = `mcp-oauth-${randomUUID2()}`;
39313
- const completed = this.codexAcpClient.awaitMcpServerOauthLoginCompleted(serverName, sessionId);
39314
- const login2 = await this.codexAcpClient.mcpServerOauthLogin({
39315
- name: serverName,
39316
- threadId: sessionId
39317
- });
39318
- const elicitation = Promise.resolve(this.connection.request(
39319
- methods.client.elicitation.create,
39320
- {
39321
- mode: "url",
39322
- sessionId,
39323
- message: `Authenticate with MCP server ${serverName}`,
39324
- url: login2.authorizationUrl,
39325
- elicitationId
39326
- }
39327
- ));
39328
- const first = await Promise.race([
39329
- completed.then((result2) => ({ type: "completed", result: result2 })),
39330
- elicitation.then((response) => ({ type: "elicitation", response }))
39331
- ]);
39332
- if (first.type === "elicitation" && !CreateElicitationResponse.isAccept(first.response)) {
39333
- return false;
39334
- }
39335
- const result = first.type === "completed" ? first.result : await completed;
39336
- await this.connection.notify(methods.client.elicitation.complete, { elicitationId });
39337
- return result.success;
39338
- }
39339
40245
  trackActivePrompt(sessionId) {
39340
40246
  let resolveCompletion = () => {
39341
40247
  };
@@ -39622,6 +40528,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
39622
40528
  return cancelledPromptResponse();
39623
40529
  }
39624
40530
  const commandPromise = this.availableCommands.tryHandleCommand(params.prompt, sessionState, {
40531
+ signal: activePrompt.signal,
39625
40532
  onTurnStartPending: () => {
39626
40533
  sessionState.lastTokenUsage = null;
39627
40534
  ensurePendingTurnStart();
@@ -40058,41 +40965,6 @@ ${stderr}` : "";
40058
40965
  await this.interruptSessionTurn(sessionState, "Cancel", false);
40059
40966
  }
40060
40967
  };
40061
- function getRequestedMcpServerNames(mcpServers) {
40062
- return Array.from(new Set(mcpServers.map((server) => sanitizeMcpServerName(server.name))));
40063
- }
40064
- var MCP_STARTUP_AWAIT_TIMEOUT_META_KEY = "mcpStartupAwaitTimeoutMs";
40065
- function parseMcpStartupAwaitTimeoutMs(meta3) {
40066
- const value = meta3?.[MCP_STARTUP_AWAIT_TIMEOUT_META_KEY];
40067
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
40068
- }
40069
- function raceMcpStartupTimeout(startup, timeoutMs) {
40070
- return new Promise((resolve, reject) => {
40071
- let settled = false;
40072
- const timer = setTimeout(() => {
40073
- if (!settled) {
40074
- settled = true;
40075
- resolve();
40076
- }
40077
- }, timeoutMs);
40078
- startup.then(
40079
- () => {
40080
- if (!settled) {
40081
- settled = true;
40082
- clearTimeout(timer);
40083
- resolve();
40084
- }
40085
- },
40086
- (err) => {
40087
- if (!settled) {
40088
- settled = true;
40089
- clearTimeout(timer);
40090
- reject(err);
40091
- }
40092
- }
40093
- );
40094
- });
40095
- }
40096
40968
  var SessionClosedDuringLoadError = class extends Error {
40097
40969
  constructor() {
40098
40970
  super("The session closed during the history load");