@agentclientprotocol/codex-acp 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +426 -20
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -24051,6 +24051,37 @@ function fallbackName(sessionId) {
24051
24051
  return `Agent ${suffix}`;
24052
24052
  }
24053
24053
 
24054
+ // src/RateLimitsMap.ts
24055
+ function rateLimitId(snapshot, fallback = "codex") {
24056
+ return snapshot.limitId ?? fallback;
24057
+ }
24058
+ function createRateLimitsMap(response) {
24059
+ const result = /* @__PURE__ */ new Map();
24060
+ const snapshots = Object.entries(response.rateLimitsByLimitId ?? {}).filter((entry) => entry[1] !== void 0);
24061
+ if (snapshots.length === 0) {
24062
+ snapshots.push([rateLimitId(response.rateLimits), response.rateLimits]);
24063
+ }
24064
+ for (const [fallbackId, snapshot] of snapshots) {
24065
+ const limitId = rateLimitId(snapshot, fallbackId);
24066
+ result.set(limitId, {
24067
+ limitId,
24068
+ limitName: snapshot.limitName ?? limitId,
24069
+ snapshot
24070
+ });
24071
+ }
24072
+ return result;
24073
+ }
24074
+ function mergeRateLimitSnapshot(previous, update) {
24075
+ return {
24076
+ ...update,
24077
+ limitId: update.limitId ?? "codex",
24078
+ credits: update.credits ?? previous.credits,
24079
+ individualLimit: update.individualLimit ?? previous.individualLimit,
24080
+ spendControlReached: update.spendControlReached ?? previous.spendControlReached,
24081
+ planType: update.planType ?? previous.planType
24082
+ };
24083
+ }
24084
+
24054
24085
  // src/CodexEventHandler.ts
24055
24086
  var MAX_SESSION_FAILURE_TITLE_LENGTH = 240;
24056
24087
  var SESSION_FAILURE_POLICY = {
@@ -24159,11 +24190,14 @@ var CodexEventHandler = class _CodexEventHandler {
24159
24190
  terminalCommandOutputIds = /* @__PURE__ */ new Set();
24160
24191
  agentMessagePhases = /* @__PURE__ */ new Map();
24161
24192
  subagents;
24193
+ /** Connection-level `authStatus` sink; the app-server account push feeds it. */
24194
+ onAccountUpdated;
24162
24195
  constructor(connection, sessionState, supportsPlanUpdates = false, supportsTypedSessionFailures = false, sessionFailureEpoch = randomUUID(), subagents = new CodexSubagentEventRouter(
24163
24196
  sessionState.sessionId,
24164
24197
  false,
24165
24198
  new ACPSessionConnection(connection, sessionState.sessionId)
24166
- )) {
24199
+ ), onAccountUpdated) {
24200
+ this.onAccountUpdated = onAccountUpdated;
24167
24201
  this.sessionState = sessionState;
24168
24202
  this.supportsPlanUpdates = supportsPlanUpdates;
24169
24203
  this.supportsTypedSessionFailures = supportsTypedSessionFailures;
@@ -24387,6 +24421,9 @@ var CodexEventHandler = class _CodexEventHandler {
24387
24421
  case "account/rateLimits/updated":
24388
24422
  this.handleRateLimitsUpdated(notification.params);
24389
24423
  return null;
24424
+ case "account/updated":
24425
+ this.onAccountUpdated?.(notification.params);
24426
+ return null;
24390
24427
  case "configWarning":
24391
24428
  return await this.createConfigWarningEvent(notification.params);
24392
24429
  case "warning":
@@ -24437,7 +24474,6 @@ var CodexEventHandler = class _CodexEventHandler {
24437
24474
  case "turn/moderationMetadata":
24438
24475
  case "item/fileChange/outputDelta":
24439
24476
  case "item/fileChange/patchUpdated":
24440
- case "account/updated":
24441
24477
  case "fs/changed":
24442
24478
  case "mcpServer/startupStatus/updated":
24443
24479
  case "mcpServer/event/stream/notification":
@@ -25083,11 +25119,13 @@ ${event.stdin}
25083
25119
  if (!this.sessionState.rateLimits) {
25084
25120
  this.sessionState.rateLimits = /* @__PURE__ */ new Map();
25085
25121
  }
25086
- const limitId = params.rateLimits.limitId ?? params.rateLimits.limitName ?? "unknown";
25122
+ const limitId = params.rateLimits.limitId ?? "codex";
25123
+ const existingEntry = this.sessionState.rateLimits.get(limitId);
25124
+ const snapshot = existingEntry ? mergeRateLimitSnapshot(existingEntry.snapshot, params.rateLimits) : { ...params.rateLimits, limitId };
25087
25125
  this.sessionState.rateLimits.set(limitId, {
25088
25126
  limitId,
25089
- limitName: params.rateLimits.limitName ?? limitId,
25090
- snapshot: params.rateLimits
25127
+ limitName: snapshot.limitName ?? existingEntry?.limitName ?? limitId,
25128
+ snapshot
25091
25129
  });
25092
25130
  }
25093
25131
  handleFuzzyFileSearchSessionUpdated(params) {
@@ -27247,7 +27285,7 @@ var package_default = {
27247
27285
  publishConfig: {
27248
27286
  access: "public"
27249
27287
  },
27250
- version: "1.8.0",
27288
+ version: "1.9.0",
27251
27289
  description: "",
27252
27290
  main: "dist/index.js",
27253
27291
  bin: {
@@ -27310,7 +27348,7 @@ var package_default = {
27310
27348
  },
27311
27349
  dependencies: {
27312
27350
  "@agentclientprotocol/sdk": "^1.4.0",
27313
- "@openai/codex": "^0.152.0",
27351
+ "@openai/codex": "^0.153.2",
27314
27352
  diff: "^9.0.0",
27315
27353
  open: "^11.0.1",
27316
27354
  "vscode-jsonrpc": "^9.0.1",
@@ -27908,6 +27946,12 @@ var CodexAcpClient = class {
27908
27946
  config;
27909
27947
  modelProvider;
27910
27948
  gatewayConfig;
27949
+ /**
27950
+ * Where the stored gateway routing came from: the `gateway` auth method
27951
+ * (agent-owned authentication) or the ACP `providers/*` API (client-driven
27952
+ * routing). `authStatus` reports only the agent-owned one.
27953
+ */
27954
+ gatewayConfigSource;
27911
27955
  pendingLoginCompleted = null;
27912
27956
  pendingAccountUpdated = null;
27913
27957
  sessionNotificationQueues = /* @__PURE__ */ new Map();
@@ -27919,6 +27963,7 @@ var CodexAcpClient = class {
27919
27963
  this.config = codexConfig ?? {};
27920
27964
  this.modelProvider = modelProvider ?? null;
27921
27965
  this.gatewayConfig = null;
27966
+ this.gatewayConfigSource = null;
27922
27967
  this.subagents = new CodexSubagentSubscriptions(codexClient);
27923
27968
  }
27924
27969
  get appServerClient() {
@@ -27951,6 +27996,7 @@ var CodexAcpClient = class {
27951
27996
  throw RequestError.invalidRequest();
27952
27997
  }
27953
27998
  this.gatewayConfig = null;
27999
+ this.gatewayConfigSource = null;
27954
28000
  switch (authRequest.methodId) {
27955
28001
  case "api-key":
27956
28002
  return await this.authenticateWithApiKey(authRequest);
@@ -28034,7 +28080,7 @@ var CodexAcpClient = class {
28034
28080
  apiType: GatewayAuthMethod._meta.gateway.protocol,
28035
28081
  headers: gatewaySettings.headers,
28036
28082
  providerName: gatewaySettings.providerName
28037
- });
28083
+ }, "authentication");
28038
28084
  return true;
28039
28085
  }
28040
28086
  readApiKeyFromEnv() {
@@ -28080,6 +28126,11 @@ var CodexAcpClient = class {
28080
28126
  };
28081
28127
  }
28082
28128
  }
28129
+ /**
28130
+ * The provider that actually serves requests, ACP-configured gateway
28131
+ * routing included. Use {@link getAgentConfiguredModelProvider} instead
28132
+ * when asking what the agent itself is configured with (`authStatus`).
28133
+ */
28083
28134
  async getCurrentModelProvider() {
28084
28135
  const sessionModelProvider = this.getModelProvider();
28085
28136
  if (sessionModelProvider !== null) {
@@ -28105,7 +28156,7 @@ var CodexAcpClient = class {
28105
28156
  * method and the ACP `providers/set` method. Throws `invalid_params` for an
28106
28157
  * unsupported protocol or a malformed base URL.
28107
28158
  */
28108
- applyGatewayConfig(params) {
28159
+ applyGatewayConfig(params, source) {
28109
28160
  const apiType = params.apiType;
28110
28161
  const wireApi = SUPPORTED_GATEWAY_PROTOCOLS[apiType];
28111
28162
  if (!wireApi) {
@@ -28122,6 +28173,7 @@ var CodexAcpClient = class {
28122
28173
  "X-Client-Feature-ID": "codex",
28123
28174
  ...params.headers
28124
28175
  };
28176
+ this.gatewayConfigSource = source;
28125
28177
  this.gatewayConfig = {
28126
28178
  modelProvider: CUSTOM_GATEWAY_PROVIDER_ID,
28127
28179
  config: {
@@ -28186,7 +28238,7 @@ var CodexAcpClient = class {
28186
28238
  apiType: request.apiType,
28187
28239
  baseUrl: request.baseUrl,
28188
28240
  headers: request.headers
28189
- });
28241
+ }, "acpProviders");
28190
28242
  logger.log("providers/set applied", {
28191
28243
  providerId: request.providerId,
28192
28244
  apiType: request.apiType,
@@ -28202,6 +28254,7 @@ var CodexAcpClient = class {
28202
28254
  const overrideWasActive = this.gatewayConfig !== null;
28203
28255
  if (request.providerId === OPENAI_PROVIDER_ID) {
28204
28256
  this.gatewayConfig = null;
28257
+ this.gatewayConfigSource = null;
28205
28258
  }
28206
28259
  const current = this.gatewayConfig ? {
28207
28260
  apiType: gatewayApiTypeFromConfig(this.gatewayConfig),
@@ -28219,6 +28272,36 @@ var CodexAcpClient = class {
28219
28272
  async getAccount() {
28220
28273
  return this.codexClient.accountRead({ refreshToken: false });
28221
28274
  }
28275
+ async getRateLimits() {
28276
+ return this.codexClient.accountRateLimitsRead();
28277
+ }
28278
+ /**
28279
+ * Presentable name of the gateway the agent itself authenticated against
28280
+ * (the `gateway` auth method), or `null`. Routing that the client
28281
+ * configured through `providers/set` is deliberately not reported here:
28282
+ * `authStatus` describes the agent-owned login only.
28283
+ */
28284
+ getAuthGatewayProviderName() {
28285
+ return this.gatewayConfigSource === "authentication" ? this.gatewayConfig?.config.name ?? null : null;
28286
+ }
28287
+ /** Whether this provider id is client-driven routing set through `providers/set`. */
28288
+ isClientConfiguredProvider(providerId) {
28289
+ return providerId === CUSTOM_GATEWAY_PROVIDER_ID && this.gatewayConfigSource === "acpProviders";
28290
+ }
28291
+ /**
28292
+ * The model provider the agent itself is configured with (launch option or
28293
+ * Codex config), ignoring any ACP-configured gateway routing. The
28294
+ * routing-aware counterpart is {@link getCurrentModelProvider}.
28295
+ */
28296
+ async getAgentConfiguredModelProvider() {
28297
+ const provider = this.getModelProvider();
28298
+ const agentProvider = this.isClientConfiguredProvider(provider) ? this.modelProvider : provider;
28299
+ if (agentProvider !== null) {
28300
+ return agentProvider;
28301
+ }
28302
+ const settingsModelProvider = await this.codexClient.configRead({ includeLayers: false });
28303
+ return settingsModelProvider?.config?.model_provider ?? null;
28304
+ }
28222
28305
  async resumeSession(request, onSubscribed) {
28223
28306
  const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta);
28224
28307
  await this.refreshSkills(request.cwd, additionalDirectories);
@@ -29473,6 +29556,9 @@ var CodexAppServerClient = class {
29473
29556
  async accountRead(params) {
29474
29557
  return await this.sendRequest({ method: "account/read", params });
29475
29558
  }
29559
+ async accountRateLimitsRead() {
29560
+ return await this.sendRequest({ method: "account/rateLimits/read", params: void 0 });
29561
+ }
29476
29562
  //TODO create type-safe helper
29477
29563
  async awaitTurnCompleted(threadId, turnId) {
29478
29564
  return await new Promise((resolve) => {
@@ -30079,6 +30165,7 @@ var CodexCommands = class {
30079
30165
  return { handled: true, turnCompleted };
30080
30166
  }
30081
30167
  case "status": {
30168
+ await this.refreshRateLimits(sessionState);
30082
30169
  const session = new ACPSessionConnection(this.connection, sessionId);
30083
30170
  const message = this.buildStatusMessage(sessionState);
30084
30171
  await session.update(createAgentTextMessageChunk(message));
@@ -30227,6 +30314,16 @@ var CodexCommands = class {
30227
30314
  ];
30228
30315
  return lines.join(" \n");
30229
30316
  }
30317
+ async refreshRateLimits(sessionState) {
30318
+ try {
30319
+ const response = await this.runWithProcessCheck(() => this.codexAcpClient.getRateLimits());
30320
+ if (response) {
30321
+ sessionState.rateLimits = createRateLimitsMap(response);
30322
+ }
30323
+ } catch (err) {
30324
+ logger.error(`Failed to refresh rate limits for session ${sessionState.sessionId}`, err);
30325
+ }
30326
+ }
30230
30327
  formatAccountInfo(account) {
30231
30328
  if (!account) {
30232
30329
  return "not logged in";
@@ -30257,10 +30354,10 @@ var CodexCommands = class {
30257
30354
  return "data not available yet";
30258
30355
  }
30259
30356
  const used = usage.totalTokens;
30260
- const percentLeft = Math.round((contextWindow - used) / contextWindow * 100);
30357
+ const percentUsed = Math.round(used / contextWindow * 100);
30261
30358
  const usedFormatted = this.formatTokenCount(used);
30262
30359
  const totalFormatted = this.formatTokenCount(contextWindow);
30263
- return `${percentLeft}% left (${usedFormatted} used / ${totalFormatted})`;
30360
+ return `${percentUsed}% used (${usedFormatted} used / ${totalFormatted})`;
30264
30361
  }
30265
30362
  formatRateLimitLines(rateLimits) {
30266
30363
  if (!rateLimits || rateLimits.size === 0) {
@@ -30294,8 +30391,31 @@ var CodexCommands = class {
30294
30391
  lines.push(`**${prefix}Credits:** ${rateLimits.credits.balance}`);
30295
30392
  }
30296
30393
  }
30394
+ if (rateLimits.individualLimit) {
30395
+ const limit = rateLimits.individualLimit;
30396
+ const used = this.formatCreditAmount(limit.used);
30397
+ const total = this.formatCreditAmount(limit.limit);
30398
+ if (used !== null && total !== null) {
30399
+ const percentLeft = Math.round(Math.min(100, Math.max(0, limit.remainingPercent)));
30400
+ const resetDate = new Date(limit.resetsAt * 1e3).toLocaleDateString("en-US", { month: "short", day: "numeric" });
30401
+ lines.push(
30402
+ `**${prefix}individual spend limit:** ${percentLeft}% left (${used} of ${total} credits used; resets ${resetDate})`
30403
+ );
30404
+ }
30405
+ }
30297
30406
  return lines;
30298
30407
  }
30408
+ formatCreditAmount(raw) {
30409
+ const trimmed = raw.trim();
30410
+ if (trimmed.length === 0) {
30411
+ return null;
30412
+ }
30413
+ const value = Number(trimmed);
30414
+ if (!Number.isFinite(value) || value < 0) {
30415
+ return null;
30416
+ }
30417
+ return Math.round(value).toLocaleString("en-US");
30418
+ }
30299
30419
  formatWindowLabel(windowDurationMins) {
30300
30420
  if (windowDurationMins === null) {
30301
30421
  return "Limit";
@@ -31342,6 +31462,139 @@ function numberValue(value) {
31342
31462
  return typeof value === "number" && Number.isFinite(value) ? value : null;
31343
31463
  }
31344
31464
 
31465
+ // src/AuthStatusMeta.ts
31466
+ var AUTH_STATUS_UPDATE_METHOD = "_auth/status_update";
31467
+ var AUTH_STATUS_META_KEY = "authStatus";
31468
+ function authStatusCapability() {
31469
+ return {};
31470
+ }
31471
+ var NOT_LOGGED_IN_LABEL = "Not logged in";
31472
+ var DEFAULT_GATEWAY_LABEL = "Custom model gateway";
31473
+ var PLAN_DISPLAY_NAMES = {
31474
+ free: "Free",
31475
+ go: "Go",
31476
+ plus: "Plus",
31477
+ pro: "Pro",
31478
+ team: "Team",
31479
+ business: "Business",
31480
+ enterprise: "Enterprise",
31481
+ edu: "Edu"
31482
+ };
31483
+ function planTypePresentable(planType) {
31484
+ if (!planType || planType === "unknown") {
31485
+ return null;
31486
+ }
31487
+ return PLAN_DISPLAY_NAMES[planType] ?? capitalize2(planType);
31488
+ }
31489
+ function capitalize2(value) {
31490
+ return value.charAt(0).toUpperCase() + value.slice(1);
31491
+ }
31492
+ function chatGptLabel(planType) {
31493
+ const plan = planTypePresentable(planType);
31494
+ return plan === null ? "ChatGPT" : `ChatGPT ${plan}`;
31495
+ }
31496
+ function gatewayStatus(providerName) {
31497
+ const detail = typeof providerName === "string" && providerName.trim().length > 0 ? providerName.trim() : void 0;
31498
+ return {
31499
+ kind: "gateway",
31500
+ label: DEFAULT_GATEWAY_LABEL,
31501
+ ...detail === void 0 ? {} : { detail }
31502
+ };
31503
+ }
31504
+ function unauthenticatedStatus() {
31505
+ return {
31506
+ kind: "none",
31507
+ label: NOT_LOGGED_IN_LABEL
31508
+ };
31509
+ }
31510
+ function fromAccount(account) {
31511
+ if (account === null) {
31512
+ return unauthenticatedStatus();
31513
+ }
31514
+ switch (account.type) {
31515
+ case "chatgpt": {
31516
+ const accountInfo = {};
31517
+ if (account.email) {
31518
+ accountInfo.email = account.email;
31519
+ }
31520
+ if (account.planType) {
31521
+ accountInfo.plan = account.planType;
31522
+ }
31523
+ return {
31524
+ kind: "account",
31525
+ label: chatGptLabel(account.planType),
31526
+ ...Object.keys(accountInfo).length > 0 ? { account: accountInfo } : {}
31527
+ };
31528
+ }
31529
+ case "apiKey":
31530
+ return {
31531
+ kind: "api_key",
31532
+ label: "OpenAI API key"
31533
+ };
31534
+ case "amazonBedrock":
31535
+ return {
31536
+ kind: "external",
31537
+ label: "AWS Bedrock"
31538
+ };
31539
+ }
31540
+ }
31541
+ function fromAccountUpdated(notification, previous) {
31542
+ const authMode = notification.authMode;
31543
+ if (authMode === null) {
31544
+ return unauthenticatedStatus();
31545
+ }
31546
+ switch (authMode) {
31547
+ case "chatgpt":
31548
+ case "chatgptAuthTokens": {
31549
+ const status = {
31550
+ kind: "account",
31551
+ label: chatGptLabel(notification.planType)
31552
+ };
31553
+ const accountInfo = {};
31554
+ const previousEmail = previous?.kind === "account" ? previous.account?.email : void 0;
31555
+ if (previousEmail) {
31556
+ accountInfo.email = previousEmail;
31557
+ }
31558
+ if (notification.planType) {
31559
+ accountInfo.plan = notification.planType;
31560
+ }
31561
+ if (Object.keys(accountInfo).length > 0) {
31562
+ status.account = accountInfo;
31563
+ }
31564
+ return status;
31565
+ }
31566
+ case "apikey":
31567
+ return {
31568
+ kind: "api_key",
31569
+ label: "OpenAI API key"
31570
+ };
31571
+ case "personalAccessToken":
31572
+ return {
31573
+ kind: "api_key",
31574
+ label: "OpenAI personal access token"
31575
+ };
31576
+ case "bedrockApiKey":
31577
+ case "bedrockAccessKeys":
31578
+ return {
31579
+ kind: "external",
31580
+ label: "AWS Bedrock"
31581
+ };
31582
+ case "agentIdentity":
31583
+ return {
31584
+ kind: "external",
31585
+ label: "Agent identity"
31586
+ };
31587
+ case "headers":
31588
+ return gatewayStatus();
31589
+ }
31590
+ }
31591
+ function sameAuthStatus(previous, next) {
31592
+ if (!previous) {
31593
+ return false;
31594
+ }
31595
+ return previous.kind === next.kind && previous.label === next.label && previous.detail === next.detail && previous.account?.email === next.account?.email && previous.account?.organization === next.account?.organization && previous.account?.plan === next.account?.plan && JSON.stringify(previous.vendor) === JSON.stringify(next.vendor);
31596
+ }
31597
+
31345
31598
  // src/AcpExtensions.ts
31346
31599
  var LEGACY_SET_SESSION_MODEL_METHOD = "session/set_model";
31347
31600
  var SESSION_STEERING_METHOD = "_session/steering";
@@ -31525,6 +31778,8 @@ var CodexAcpServer = class _CodexAcpServer {
31525
31778
  clientCapabilities;
31526
31779
  terminalOutputMode;
31527
31780
  booleanConfigOptionsSupported;
31781
+ /** Last `authStatus` pushed to the client; used to suppress duplicates. */
31782
+ currentAuthStatus;
31528
31783
  sessions;
31529
31784
  pendingMcpStartupSessions;
31530
31785
  pendingTurnStarts;
@@ -31561,6 +31816,7 @@ var CodexAcpServer = class _CodexAcpServer {
31561
31816
  this.clientCapabilities = null;
31562
31817
  this.terminalOutputMode = "terminal_output_delta";
31563
31818
  this.booleanConfigOptionsSupported = false;
31819
+ this.currentAuthStatus = null;
31564
31820
  this.availableCommands = this.createAvailableCommands(codexAcpClient);
31565
31821
  }
31566
31822
  createAvailableCommands(client) {
@@ -31568,7 +31824,7 @@ var CodexAcpServer = class _CodexAcpServer {
31568
31824
  this.connection,
31569
31825
  client,
31570
31826
  (operation) => this.runWithProcessCheck(operation),
31571
- () => this.refreshSessionsAuthState(null)
31827
+ () => this.refreshAuthState(null)
31572
31828
  );
31573
31829
  }
31574
31830
  async initialize(_params) {
@@ -31579,6 +31835,7 @@ var CodexAcpServer = class _CodexAcpServer {
31579
31835
  this.terminalOutputMode = resolveTerminalOutputMode(_params.clientCapabilities);
31580
31836
  this.booleanConfigOptionsSupported = clientSupportsBooleanConfigOptions(_params.clientCapabilities);
31581
31837
  await this.runWithProcessCheck(() => this.codexAcpClient.initialize(_params));
31838
+ this.publishFirstAuthStatusAfterResponse();
31582
31839
  const sessionCapabilities = {
31583
31840
  resume: {},
31584
31841
  list: {},
@@ -31610,6 +31867,11 @@ var CodexAcpServer = class _CodexAcpServer {
31610
31867
  acp: false,
31611
31868
  http: true,
31612
31869
  sse: false
31870
+ },
31871
+ _meta: {
31872
+ // Presence means "this agent pushes `_auth/status_update`". It
31873
+ // never carries a payload, and the client never asks for one.
31874
+ [AUTH_STATUS_META_KEY]: authStatusCapability()
31613
31875
  }
31614
31876
  },
31615
31877
  authMethods: getCodexAuthMethods(_params.clientCapabilities),
@@ -31741,7 +32003,7 @@ var CodexAcpServer = class _CodexAcpServer {
31741
32003
  async handleError(e) {
31742
32004
  if (e.message.includes("log out") || e.message.includes("cloud requirements")) {
31743
32005
  await this.runWithProcessCheck(() => this.codexAcpClient.logout());
31744
- await this.refreshSessionsAuthState(null);
32006
+ await this.refreshAuthState(null);
31745
32007
  throw RequestError.internalError(`${e.message}
31746
32008
 
31747
32009
  You have been logged out. Please try again.`);
@@ -31922,12 +32184,14 @@ Check ${configPath} and project .codex directories, especially their config.toml
31922
32184
  }
31923
32185
  async getAuthStateForProvider(authProvider) {
31924
32186
  if (!this.authProviderUsesOpenAiAccount(authProvider)) {
32187
+ await this.publishAuthStatus(authProvider, null);
31925
32188
  return {
31926
32189
  account: null,
31927
32190
  authConfigured: true
31928
32191
  };
31929
32192
  }
31930
32193
  const accountResponse = await this.runWithProcessCheck(() => this.codexAcpClient.getAccount());
32194
+ await this.publishAuthStatus(authProvider, accountResponse.account);
31931
32195
  return {
31932
32196
  account: accountResponse.account,
31933
32197
  authConfigured: accountResponse.account !== null || !accountResponse.requiresOpenaiAuth
@@ -32106,7 +32370,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
32106
32370
  logger.log("Authenticate request failed");
32107
32371
  throw RequestError.invalidParams();
32108
32372
  }
32109
- await this.refreshSessionsAuthState(this.getAuthProviderForAuthenticateRequest(_params));
32373
+ await this.refreshAuthState(this.getAuthProviderForAuthenticateRequest(_params));
32110
32374
  logger.log("Authenticate request completed");
32111
32375
  return {};
32112
32376
  }
@@ -32137,7 +32401,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
32137
32401
  async logout(_params) {
32138
32402
  logger.log("Logout request received");
32139
32403
  await this.runWithProcessCheck(() => this.codexAcpClient.logout());
32140
- await this.refreshSessionsAuthState(null);
32404
+ await this.refreshAuthState(null);
32141
32405
  logger.log("Logout request completed");
32142
32406
  }
32143
32407
  listProviders(_params) {
@@ -32240,15 +32504,155 @@ Check ${configPath} and project .codex directories, especially their config.toml
32240
32504
  state.modelProvider
32241
32505
  );
32242
32506
  }
32507
+ /** Returns whether the auth state was read (and thus the auth status pushed). */
32243
32508
  async refreshSessionsAuthState(authProvider) {
32244
- if (this.sessions.size === 0) return;
32509
+ if (this.sessions.size === 0) return false;
32245
32510
  const sessionsToRefresh = [...this.sessions.values()].filter((sessionState) => this.authProvidersMatch(sessionState.authProvider, authProvider));
32246
- if (sessionsToRefresh.length === 0) return;
32511
+ if (sessionsToRefresh.length === 0) return false;
32247
32512
  const authState = await this.getAuthStateForProvider(authProvider);
32248
32513
  for (const sessionState of sessionsToRefresh) {
32249
32514
  sessionState.account = authState.account;
32250
32515
  sessionState.authConfigured = authState.authConfigured;
32251
32516
  }
32517
+ return true;
32518
+ }
32519
+ /**
32520
+ * Refreshes the sessions of a provider and makes sure the connection-level
32521
+ * `authStatus` is pushed even when no session matched (the empty-screen
32522
+ * login case). Reuses the session refresh read; never adds a second one.
32523
+ */
32524
+ async refreshAuthState(authProvider) {
32525
+ const refreshed = await this.refreshSessionsAuthState(authProvider);
32526
+ if (refreshed) return;
32527
+ try {
32528
+ await this.getAuthStateForProvider(authProvider ?? this.codexAcpClient.getModelProvider());
32529
+ } catch (error51) {
32530
+ logger.log("Failed to refresh auth status", { error: String(error51) });
32531
+ }
32532
+ }
32533
+ /**
32534
+ * Schedules the connection's first `_auth/status_update`: one account read,
32535
+ * pushed whatever it says, including `none`.
32536
+ *
32537
+ * The push must not overtake the `initialize` response. The JSON-RPC layer
32538
+ * writes that response in the microtask that resolves {@link initialize}, so
32539
+ * the read starts from a check-phase callback, which always runs after it.
32540
+ * `initialize` itself never waits for the read.
32541
+ *
32542
+ * "Unconditional" costs nothing extra here: nothing has been pushed yet on
32543
+ * this connection, so {@link setAuthStatus} cannot suppress this one.
32544
+ */
32545
+ publishFirstAuthStatusAfterResponse() {
32546
+ setImmediate(() => void this.publishAuthStatusRead());
32547
+ }
32548
+ /**
32549
+ * Reads the agent-owned identity and pushes it.
32550
+ *
32551
+ * Never rejects: an unreadable source means "nothing to report", not an
32552
+ * error. The client then keeps showing the last pushed value, or "not
32553
+ * reported" when there was none.
32554
+ */
32555
+ async publishAuthStatusRead() {
32556
+ let authStatus;
32557
+ try {
32558
+ authStatus = await this.readAgentAuthIdentity();
32559
+ } catch (error51) {
32560
+ logger.log("Cannot determine auth status", { error: String(error51) });
32561
+ return;
32562
+ }
32563
+ await this.setAuthStatus(authStatus);
32564
+ }
32565
+ /**
32566
+ * Builds the agent-owned auth identity. Routing the client configured
32567
+ * through the ACP `providers/*` API is invisible here: the reported state
32568
+ * is what the agent itself is logged in with. `gateway` stays reserved for
32569
+ * agent-owned gateway state — the `gateway` auth method, or a provider the
32570
+ * user configured in Codex's own config.
32571
+ */
32572
+ async readAgentAuthIdentity() {
32573
+ const authGatewayName = this.codexAcpClient.getAuthGatewayProviderName();
32574
+ if (authGatewayName !== null) {
32575
+ return gatewayStatus(authGatewayName);
32576
+ }
32577
+ const modelProvider = await this.runWithProcessCheck(() => this.codexAcpClient.getAgentConfiguredModelProvider());
32578
+ if (!this.authProviderUsesOpenAiAccount(modelProvider)) {
32579
+ return gatewayStatus(modelProvider);
32580
+ }
32581
+ const accountResponse = await this.runWithProcessCheck(() => this.codexAcpClient.getAccount());
32582
+ return fromAccount(accountResponse.account);
32583
+ }
32584
+ /**
32585
+ * Pushes `_auth/status_update` for the freshly read account of a provider.
32586
+ * Agent-owned gateway authentication wins; a client-driven provider
32587
+ * override is ignored and the agent-owned login is reported instead.
32588
+ */
32589
+ async publishAuthStatus(authProvider, account) {
32590
+ const authGatewayName = this.codexAcpClient.getAuthGatewayProviderName();
32591
+ if (authGatewayName !== null) {
32592
+ await this.setAuthStatus(gatewayStatus(authGatewayName));
32593
+ return;
32594
+ }
32595
+ if (this.authProviderUsesOpenAiAccount(authProvider)) {
32596
+ await this.setAuthStatus(fromAccount(account));
32597
+ return;
32598
+ }
32599
+ if (this.codexAcpClient.isClientConfiguredProvider(authProvider)) {
32600
+ await this.publishAuthStatusRead();
32601
+ return;
32602
+ }
32603
+ await this.setAuthStatus(gatewayStatus(authProvider));
32604
+ }
32605
+ /**
32606
+ * Handles the app-server `account/updated` push: the free freshness channel
32607
+ * for logins and logouts happening outside this connection.
32608
+ */
32609
+ handleAccountUpdated(notification) {
32610
+ void this.applyAccountUpdated(notification);
32611
+ }
32612
+ /**
32613
+ * `account/updated` describes the Codex account only. It must never
32614
+ * overwrite an agent-owned gateway status, which no account event can
32615
+ * invalidate; only a gateway logout or a provider change does.
32616
+ */
32617
+ async applyAccountUpdated(notification) {
32618
+ try {
32619
+ if (this.codexAcpClient.getAuthGatewayProviderName() !== null) {
32620
+ return;
32621
+ }
32622
+ if (this.currentAuthStatus === null) {
32623
+ await this.publishAuthStatusRead();
32624
+ return;
32625
+ }
32626
+ if (this.currentAuthStatus.kind === "gateway") {
32627
+ return;
32628
+ }
32629
+ await this.setAuthStatus(fromAccountUpdated(notification, this.currentAuthStatus));
32630
+ } catch (error51) {
32631
+ logger.log("Failed to apply account update to auth status", { error: String(error51) });
32632
+ }
32633
+ }
32634
+ /**
32635
+ * Stores `next` and pushes `_auth/status_update`.
32636
+ *
32637
+ * A push goes out only when the payload changed. The identity is read on
32638
+ * many occasions — `initialize`, each session create, each `account/updated`
32639
+ * — and almost all of them see the login already reported.
32640
+ * Clients replace their whole state on each update and tolerate duplicates,
32641
+ * so a repeat is harmless, but it is pure noise all the same.
32642
+ *
32643
+ * The first push of a connection always goes out: nothing was reported yet,
32644
+ * so no payload can equal it.
32645
+ */
32646
+ async setAuthStatus(next) {
32647
+ if (sameAuthStatus(this.currentAuthStatus, next)) {
32648
+ return;
32649
+ }
32650
+ this.currentAuthStatus = next;
32651
+ try {
32652
+ await this.connection.notify(AUTH_STATUS_UPDATE_METHOD, { authStatus: next });
32653
+ } catch (error51) {
32654
+ logger.log("Failed to send auth status update", { error: String(error51) });
32655
+ }
32252
32656
  }
32253
32657
  async setSessionMode(_params) {
32254
32658
  logger.log("Set session mode requested", {
@@ -33438,7 +33842,6 @@ Check ${configPath} and project .codex directories, especially their config.toml
33438
33842
  let promptWasCancelled = false;
33439
33843
  let recoverableSessionFailure = sessionState.sessionFailure;
33440
33844
  sessionState.currentTurnId = null;
33441
- sessionState.lastTokenUsage = null;
33442
33845
  const activePrompt = this.trackActivePrompt(params.sessionId);
33443
33846
  let pendingTurnStart = null;
33444
33847
  const ensurePendingTurnStart = () => {
@@ -33471,7 +33874,8 @@ Check ${configPath} and project .codex directories, especially their config.toml
33471
33874
  clientSupportsPlanUpdates(this.clientCapabilities),
33472
33875
  clientSupportsTypedSessionFailures(this.clientCapabilities),
33473
33876
  this.sessionFailureEpoch,
33474
- sessionState.subagents
33877
+ sessionState.subagents,
33878
+ (accountUpdated) => this.handleAccountUpdated(accountUpdated)
33475
33879
  );
33476
33880
  eventHandler = promptEventHandler;
33477
33881
  const permissionLifecycle = this.permissionLifecycleContext(sessionState);
@@ -33516,6 +33920,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
33516
33920
  }
33517
33921
  const commandPromise = this.availableCommands.tryHandleCommand(params.prompt, sessionState, {
33518
33922
  onTurnStartPending: () => {
33923
+ sessionState.lastTokenUsage = null;
33519
33924
  ensurePendingTurnStart();
33520
33925
  },
33521
33926
  onTurnStarted: (turnId, threadId) => {
@@ -33612,6 +34017,7 @@ Check ${configPath} and project .codex directories, especially their config.toml
33612
34017
  sessionState.fastModeEnabled,
33613
34018
  sessionState.currentModelSupportsFast
33614
34019
  );
34020
+ sessionState.lastTokenUsage = null;
33615
34021
  ensurePendingTurnStart();
33616
34022
  const sendPromptPromise = this.runWithProcessCheck(
33617
34023
  () => this.codexAcpClient.sendPrompt(
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.8.0",
6
+ "version": "1.9.0",
7
7
  "description": "",
8
8
  "main": "dist/index.js",
9
9
  "bin": {
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@agentclientprotocol/sdk": "^1.4.0",
69
- "@openai/codex": "^0.152.0",
69
+ "@openai/codex": "^0.153.2",
70
70
  "diff": "^9.0.0",
71
71
  "open": "^11.0.1",
72
72
  "vscode-jsonrpc": "^9.0.1",