codex-multi-auth 2.8.4 → 2.8.5

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.8.4",
3
+ "version": "2.8.5",
4
4
  "description": "Install and operate codex-multi-auth for the official @openai/codex CLI with multi-account OAuth rotation, switching, health checks, and recovery tools.",
5
5
  "interface": {
6
6
  "composerIcon": "./assets/codex-multi-auth-icon.svg"
package/README.md CHANGED
@@ -257,7 +257,7 @@ For remote or headless shells, prefer `codex-multi-auth login --device-auth`.
257
257
  | Routing profiles | `~/.codex/multi-auth/routing-profiles.json` |
258
258
  | Budget guards | `~/.codex/multi-auth/budget-guards.json` |
259
259
  | Local client tokens | `~/.codex/multi-auth/local-client-tokens.json` |
260
- | Runtime app helper status | `~/.codex/multi-auth/runtime-rotation-app-helper.json` |
260
+ | Runtime app helper status | `~/.codex/multi-auth/runtime-rotation-app-helper.<pid>.json` |
261
261
  | Runtime app helper owner metadata | `~/.codex/multi-auth/runtime-rotation-app-helper-owner.<pid>.json` |
262
262
  | Persistent app bind state/logs | `~/.codex/multi-auth/app-bind/` |
263
263
  | Logs | `~/.codex/multi-auth/logs/codex-plugin/` |
@@ -392,9 +392,9 @@ codex-multi-auth doctor --json
392
392
 
393
393
  ## Release Notes
394
394
 
395
- - Current stable: [docs/releases/v2.8.4.md](docs/releases/v2.8.4.md) — install via `npm i -g codex-multi-auth`
396
- - Previous stable: [docs/releases/v2.8.3.md](docs/releases/v2.8.3.md)
397
- - Previous stable: [docs/releases/v2.8.2.md](docs/releases/v2.8.2.md)
395
+ - Current stable: [docs/releases/v2.8.5.md](docs/releases/v2.8.5.md) — install via `npm i -g codex-multi-auth`
396
+ - Previous stable: [docs/releases/v2.8.3.md](docs/releases/v2.8.3.md)
397
+ - Previous stable: [docs/releases/v2.8.2.md](docs/releases/v2.8.2.md)
398
398
  - Previous stable: [docs/releases/v2.7.1.md](docs/releases/v2.7.1.md)
399
399
  - Previous stable: [docs/releases/v2.7.0.md](docs/releases/v2.7.0.md)
400
400
  - Previous stable: [docs/releases/v2.6.0.md](docs/releases/v2.6.0.md)
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../lib/auth/auth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEX,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,UAAU,EACV,MAAM,aAAa,CAAC;AAMrB,eAAO,MAAM,SAAS,iCAAiC,CAAC;AACxD,eAAO,MAAM,aAAa,4CAA4C,CAAC;AACvE,eAAO,MAAM,SAAS,wCAAwC,CAAC;AAC/D,eAAO,MAAM,YAAY,wCAAwC,CAAC;AAClE,eAAO,MAAM,KAAK,wCAAwC,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa;;;;;;EAYzB,CAAC;AA0EF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAyBvE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAY3D;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,CAoCtE;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC9C,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,WAAW,GAAE,MAAqB,GAChC,OAAO,CAAC,WAAW,CAAC,CAwDtB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAe1D;AAED;;;;GAIG;AACH,KAAK,yBAAyB,GAAG;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,CAAC;AAEF,wBAAsB,kBAAkB,CACvC,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,yBAA8B,GACrC,OAAO,CAAC,WAAW,CAAC,CAsEtB;AAED,MAAM,WAAW,wBAAwB;IACxC;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC5C,OAAO,CAAC,EAAE,wBAAwB,GAChC,OAAO,CAAC,iBAAiB,CAAC,CAuB5B"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../lib/auth/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEX,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,UAAU,EACV,MAAM,aAAa,CAAC;AAMrB,eAAO,MAAM,SAAS,iCAAiC,CAAC;AACxD,eAAO,MAAM,aAAa,4CAA4C,CAAC;AACvE,eAAO,MAAM,SAAS,wCAAwC,CAAC;AAC/D,eAAO,MAAM,YAAY,wCAAwC,CAAC;AAClE,eAAO,MAAM,KAAK,wCAAwC,CAAC;AAE3D;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa;;;;;;EAYzB,CAAC;AA0EF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAyBvE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAY3D;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,CAoCtE;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC9C,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,WAAW,GAAE,MAAqB,GAChC,OAAO,CAAC,WAAW,CAAC,CAwDtB;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAe1D;AAED;;;;GAIG;AACH,KAAK,yBAAyB,GAAG;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB,CAAC;AAEF,wBAAsB,kBAAkB,CACvC,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,yBAA8B,GACrC,OAAO,CAAC,WAAW,CAAC,CAsEtB;AAED,MAAM,WAAW,wBAAwB;IACxC;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB;AAYD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC5C,OAAO,CAAC,EAAE,wBAAwB,GAChC,OAAO,CAAC,iBAAiB,CAAC,CAuB5B"}
@@ -1,5 +1,4 @@
1
- import { generatePKCE } from "@openauthjs/openauth/pkce";
2
- import { randomBytes } from "node:crypto";
1
+ import { randomBytes, webcrypto } from "node:crypto";
3
2
  import { logError } from "../logger.js";
4
3
  import { safeParseOAuthTokenResponse } from "../schemas.js";
5
4
  import { isAbortError } from "../utils.js";
@@ -340,13 +339,19 @@ export async function refreshAccessToken(refreshToken, options = {}) {
340
339
  return { type: "failed", reason: "network_error", message: err?.message };
341
340
  }
342
341
  }
342
+ async function generatePKCE() {
343
+ const verifier = randomBytes(64).toString("base64url");
344
+ const digest = await webcrypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
345
+ const challenge = Buffer.from(digest).toString("base64url");
346
+ return { verifier, challenge };
347
+ }
343
348
  /**
344
349
  * Create OAuth authorization flow
345
350
  * @param options - Optional configuration for the flow
346
351
  * @returns Authorization flow details
347
352
  */
348
353
  export async function createAuthorizationFlow(options) {
349
- const pkce = (await generatePKCE());
354
+ const pkce = await generatePKCE();
350
355
  const state = createState();
351
356
  const url = new URL(AUTHORIZE_URL);
352
357
  url.searchParams.set("response_type", "code");
@@ -1 +1 @@
1
- {"version":3,"file":"rotation.d.ts","sourceRoot":"","sources":["../../../../lib/codex-manager/commands/rotation.ts"],"names":[],"mappings":"AA4BA,OAAO,EAEN,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,2BAA2B,CAAC;AAMnC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAEN,KAAK,4BAA4B,EACjC,MAAM,wCAAwC,CAAC;AAOhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAqB,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAG5E,KAAK,aAAa,GAAG,gBAAgB,GAAG,IAAI,CAAC;AAiB7C,MAAM,WAAW,mBAAmB;IACnC,gBAAgB,EAAE,MAAM,YAAY,CAAC;IACrC,gBAAgB,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,4BAA4B,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,OAAO,CAAC;IAChE,YAAY,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAC3C,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,kBAAkB,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,CAAC;IAC1D,cAAc,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACpC,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC9C,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5C,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAC9C,qBAAqB,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IACrD,gCAAgC,CAAC,EAAE,MAAM,OAAO,CAAC,4BAA4B,GAAG,IAAI,CAAC,CAAC;IACtF,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACtD,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAmnBD,wBAAsB,kBAAkB,CACvC,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE,mBAAmB,GACvB,OAAO,CAAC,MAAM,CAAC,CAkFjB"}
1
+ {"version":3,"file":"rotation.d.ts","sourceRoot":"","sources":["../../../../lib/codex-manager/commands/rotation.ts"],"names":[],"mappings":"AA2BA,OAAO,EAEN,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,2BAA2B,CAAC;AAMnC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAEN,KAAK,4BAA4B,EACjC,MAAM,wCAAwC,CAAC;AAahD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAqB,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAG5E,KAAK,aAAa,GAAG,gBAAgB,GAAG,IAAI,CAAC;AAoB7C,MAAM,WAAW,mBAAmB;IACnC,gBAAgB,EAAE,MAAM,YAAY,CAAC;IACrC,gBAAgB,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,4BAA4B,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,OAAO,CAAC;IAChE,YAAY,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAC3C,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,kBAAkB,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,CAAC;IAC1D,cAAc,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACpC,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC9C,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5C,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAC9C,qBAAqB,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IACrD,gCAAgC,CAAC,EAAE,MAAM,OAAO,CAAC,4BAA4B,GAAG,IAAI,CAAC,CAAC;IACtF,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACtD,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AA4pBD,wBAAsB,kBAAkB,CACvC,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE,mBAAmB,GACvB,OAAO,CAAC,MAAM,CAAC,CAkFjB"}
@@ -1,5 +1,4 @@
1
- import { existsSync, readFileSync, statSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
2
  import { AccountManager, formatAccountLabel, formatCooldown, formatWaitTime, } from "../../accounts.js";
4
3
  import { parseBooleanEnv } from "../../env-parsing.js";
5
4
  import { getCodexMultiAuthDir } from "../../runtime-paths.js";
@@ -18,10 +17,11 @@ function redactedResetRateLimitsLabel(account, index) {
18
17
  return `account ${index + 1} (id:${tail})`;
19
18
  }
20
19
  import { formatAppBindStatus, } from "../../runtime/app-bind.js";
21
- import { APP_RUNTIME_HELPER_STATUS_FILE } from "../../runtime-constants.js";
20
+ import { listRuntimeHelperStatusPaths } from "../../runtime-constants.js";
22
21
  import { findQuotaCacheEntryForAccount, isQuotaCacheEntryExhausted, } from "../../quota-readiness.js";
23
22
  import { recordRuntimeReset, } from "../../runtime/runtime-observability.js";
24
23
  import { appRuntimeHelperStatusToSignal as appRuntimeHelperStatusToRuntimeSignal, resolveAccountCurrentMarkers, resolveRuntimeCurrentAccount, } from "../../runtime/runtime-current-account.js";
24
+ import { isLiveRuntimeHelper, liveRuntimeHelpers, readRuntimeHelperPid, selectRuntimeHelperStatus, } from "../../runtime/app-helper-selection.js";
25
25
  import { isRateLimitedMarker } from "../rate-limit-markers.js";
26
26
  import { isRecord } from "../../utils.js";
27
27
  function printRotationUsage(logInfo) {
@@ -402,8 +402,7 @@ function readOptionalString(record, key) {
402
402
  : null;
403
403
  }
404
404
  const MAX_STATUS_FILE_BYTES = 1024 * 1024; // 1 MB sanity cap
405
- function readAppRuntimeHelperStatus() {
406
- const statusPath = join(getCodexMultiAuthDir(), APP_RUNTIME_HELPER_STATUS_FILE);
405
+ function readAppRuntimeHelperStatusFile(statusPath) {
407
406
  if (!existsSync(statusPath))
408
407
  return null;
409
408
  try {
@@ -421,7 +420,8 @@ function readAppRuntimeHelperStatus() {
421
420
  return {
422
421
  state: readOptionalString(parsed, "state"),
423
422
  kind: readOptionalString(parsed, "kind"),
424
- pid: readOptionalNumber(parsed, "pid"),
423
+ pid: readRuntimeHelperPid(parsed.pid),
424
+ startedAt: readOptionalNumber(parsed, "startedAt"),
425
425
  idleExpiresAt: readOptionalNumber(parsed, "idleExpiresAt"),
426
426
  totalRequests: readOptionalNumber(parsed, "totalRequests"),
427
427
  rotations: readOptionalNumber(parsed, "rotations"),
@@ -437,17 +437,24 @@ function readAppRuntimeHelperStatus() {
437
437
  return null;
438
438
  }
439
439
  }
440
- function isProcessAlive(pid) {
441
- if (!pid)
442
- return false;
440
+ // One directory scan feeds both the status line and the live-helper count so
441
+ // the two cannot observe different moments; path discovery is shared with
442
+ // every other reader via listRuntimeHelperStatusPaths in runtime-constants.
443
+ function readAppRuntimeHelperStatuses() {
444
+ const multiAuthDir = getCodexMultiAuthDir();
445
+ let entries = [];
443
446
  try {
444
- process.kill(pid, 0);
445
- return true;
447
+ entries = readdirSync(multiAuthDir);
446
448
  }
447
- catch (error) {
448
- const code = error && typeof error === "object" && "code" in error ? error.code : null;
449
- return code === "EPERM";
449
+ catch {
450
+ entries = [];
450
451
  }
452
+ return listRuntimeHelperStatusPaths(multiAuthDir, entries)
453
+ .map(readAppRuntimeHelperStatusFile)
454
+ .filter((status) => status !== null && status.kind === "codex-app-runtime-rotation-helper");
455
+ }
456
+ function readAppRuntimeHelperStatus(now = Date.now()) {
457
+ return selectRuntimeHelperStatus(readAppRuntimeHelperStatuses(), now);
451
458
  }
452
459
  function formatHelperLastAccount(status) {
453
460
  if (status.lastAccountLabel && !status.lastAccountLabel.includes("@")) {
@@ -463,14 +470,19 @@ function formatHelperLastAccount(status) {
463
470
  }
464
471
  return null;
465
472
  }
466
- function formatAppRuntimeHelperStatus(now, status = readAppRuntimeHelperStatus()) {
473
+ function formatAppRuntimeHelperStatus(now, status = readAppRuntimeHelperStatus(now), liveHelperCount = status ? 1 : 0) {
467
474
  if (!status)
468
475
  return "Codex app helper: not running";
469
476
  if (status.kind !== "codex-app-runtime-rotation-helper") {
470
477
  return "Codex app helper: not running";
471
478
  }
472
- const alive = isProcessAlive(status.pid);
473
- if (!alive || status.state === "stopped" || status.state === "idle-timeout") {
479
+ // Only "running" is running: "stopped", "idle-timeout", "max-lifetime",
480
+ // "owner-gone", "error", and anything a future helper invents are all
481
+ // terminal, and a live kill(pid, 0) on a terminal record proves nothing —
482
+ // the PID may be recycled, which is the exact gate this fix stopped
483
+ // trusting. `isLiveRuntimeHelper` folds both conditions together, and is
484
+ // the same predicate the selection above and the account marker below use.
485
+ if (!isLiveRuntimeHelper(status, now)) {
474
486
  return "Codex app helper: not running";
475
487
  }
476
488
  const parts = [`running${status.pid ? ` pid=${status.pid}` : ""}`];
@@ -484,6 +496,12 @@ function formatAppRuntimeHelperStatus(now, status = readAppRuntimeHelperStatus()
484
496
  if (status.idleExpiresAt !== null && status.idleExpiresAt > now) {
485
497
  parts.push(`idle-expires=${formatWaitTime(status.idleExpiresAt - now)}`);
486
498
  }
499
+ // The line shows the most recently active helper; with per-account
500
+ // app-servers several can be live at once, so say so instead of implying
501
+ // this one is the only one.
502
+ if (liveHelperCount > 1) {
503
+ parts.push(`(+${liveHelperCount - 1} more running)`);
504
+ }
487
505
  return `Codex app helper: ${parts.join(", ")}`;
488
506
  }
489
507
  function shouldAutoBindCodexApp(env = process.env) {
@@ -532,8 +550,13 @@ async function printRotationStatus(deps) {
532
550
  logInfo(`Runtime rotation proxy: ${enabled ? "enabled" : "disabled"}`);
533
551
  logInfo(`Stored setting: ${config.codexRuntimeRotationProxy === true ? "enabled" : "disabled"}`);
534
552
  logInfo(`Env override: ${formatEnvOverride()}`);
535
- const helperStatus = readAppRuntimeHelperStatus();
536
- logInfo(formatAppRuntimeHelperStatus(now, helperStatus));
553
+ // One scan and one selection feed the status line, the live count and the
554
+ // account marker below. Selecting twice re-ran the liveness probes at a
555
+ // later instant, so the helper named on the line and the helper whose
556
+ // account is marked `current` could be different processes (#667).
557
+ const helperStatuses = readAppRuntimeHelperStatuses();
558
+ const selectedHelperStatus = selectRuntimeHelperStatus(helperStatuses, now);
559
+ logInfo(formatAppRuntimeHelperStatus(now, selectedHelperStatus, liveRuntimeHelpers(helperStatuses, now).length));
537
560
  const appBindStatus = await printCodexAppBindStatus(deps);
538
561
  logInfo(`Storage: ${storagePath}`);
539
562
  if (!storage || storage.accounts.length === 0) {
@@ -552,7 +575,7 @@ async function printRotationStatus(deps) {
552
575
  const runtimeCurrent = resolveRuntimeCurrentAccount(storage, {
553
576
  runtimeSnapshot,
554
577
  appBindStatus: appBindStatus?.running ? appBindStatus.router : null,
555
- appHelperStatus: appRuntimeHelperStatusToRuntimeSignal(helperStatus),
578
+ appHelperStatus: appRuntimeHelperStatusToRuntimeSignal(selectedHelperStatus, now),
556
579
  }, { now });
557
580
  logInfo(`Accounts: ${storage.accounts.length}`);
558
581
  for (let index = 0; index < storage.accounts.length; index += 1) {
@@ -111,6 +111,13 @@ export declare function stopRuntimeRotationRouterProcess(router: Pick<AppBindRou
111
111
  /** Parse the documented local-time format emitted by `ps -o lstart=`. */
112
112
  export declare function parsePosixProcessStartTime(value: string): number | null;
113
113
  export declare function stopDetachedProcess(pid: number | null, platform: NodeJS.Platform, options?: DetachedProcessStopOptions): Promise<boolean>;
114
+ /**
115
+ * How many helper records unbind processes at once. Exported so a regression
116
+ * test can observe the bound rather than only its effects — with a handful of
117
+ * records any width behaves identically, so an edit to `Infinity` would
118
+ * otherwise ship green.
119
+ */
120
+ export declare const UNBIND_HELPER_CONCURRENCY = 8;
114
121
  export declare function stopRuntimeRotationAppHelperProcess(helper: RuntimeRotationAppHelperStatus, options?: DetachedProcessStopOptions & {
115
122
  platform?: NodeJS.Platform;
116
123
  }): Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"app-bind.d.ts","sourceRoot":"","sources":["../../../lib/runtime/app-bind.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app-bind.d.ts","sourceRoot":"","sources":["../../../lib/runtime/app-bind.ts"],"names":[],"mappings":"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"}
@@ -1,14 +1,14 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
3
  import { closeSync, existsSync, mkdirSync, openSync } from "node:fs";
4
- import { mkdir, open, readFile, rename, rm, unlink } from "node:fs/promises";
4
+ import { mkdir, open, readFile, readdir, rename, rm, unlink } from "node:fs/promises";
5
5
  import { homedir } from "node:os";
6
6
  import { basename, dirname, join } from "node:path";
7
7
  import process from "node:process";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { withFileOperationRetry } from "../fs-retry.js";
10
10
  import { getCodexMultiAuthDir } from "../runtime-paths.js";
11
- import { APP_RUNTIME_HELPER_OWNER_FILE, APP_RUNTIME_HELPER_STATUS_FILE, } from "../runtime-constants.js";
11
+ import { APP_RUNTIME_HELPER_OWNER_FILE, listRuntimeHelperOwnerPaths, listRuntimeHelperStatusPaths, } from "../runtime-constants.js";
12
12
  import { configHasRuntimeRotationProvider, restoreConfigTomlFromRuntimeRotationProvider, restoreConfigTomlFromRuntimeRotationProviderWithoutBackup, rewriteConfigTomlForRuntimeRotationProvider, } from "./config-toml.js";
13
13
  const APP_BIND_DIR_NAME = "app-bind";
14
14
  const APP_BIND_STATE_FILE = "runtime-rotation-app-bind.json";
@@ -879,6 +879,44 @@ function resolveRuntimeHelperOwnerPath(baseDir, pid) {
879
879
  return null;
880
880
  return join(baseDir, APP_RUNTIME_HELPER_OWNER_FILE.replace(/\.json$/i, `.${pid}.json`));
881
881
  }
882
+ /**
883
+ * How many helper records unbind processes at once. Exported so a regression
884
+ * test can observe the bound rather than only its effects — with a handful of
885
+ * records any width behaves identically, so an edit to `Infinity` would
886
+ * otherwise ship green.
887
+ */
888
+ export const UNBIND_HELPER_CONCURRENCY = 8;
889
+ /**
890
+ * `Promise.all` over `items` with at most `limit` in flight, preserving input
891
+ * order in the result. Used where the per-item work is independent but not
892
+ * free — a helper stop pays a SIGTERM, a graceful wait and possibly a SIGKILL —
893
+ * so serialising it multiplies a single stop window by the number of stale
894
+ * helpers, and unbounded parallelism signals every one of them at once.
895
+ */
896
+ async function mapWithConcurrency(items, limit, worker) {
897
+ const results = new Array(items.length);
898
+ let next = 0;
899
+ const runners = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
900
+ for (;;) {
901
+ const index = next;
902
+ next += 1;
903
+ // Only running past the end retires a runner. Folding the
904
+ // `undefined` check into the same `return` would make a sparse
905
+ // array or a `(T | undefined)[]` silently drop every item after
906
+ // the first hole — on a cleanup path whose failure mode is
907
+ // "helpers left running while the user is told the app was
908
+ // unbound".
909
+ if (index >= items.length)
910
+ return;
911
+ const item = items[index];
912
+ if (item === undefined)
913
+ continue;
914
+ results[index] = await worker(item, index);
915
+ }
916
+ });
917
+ await Promise.all(runners);
918
+ return results;
919
+ }
882
920
  export async function stopRuntimeRotationAppHelperProcess(helper, options = {}) {
883
921
  if (helper.kind !== "codex-app-runtime-rotation-helper" ||
884
922
  helper.state !== "running" ||
@@ -1054,66 +1092,146 @@ async function unbindCodexAppRuntimeRotationLocked(options, paths) {
1054
1092
  if (router?.pid && (!routerStopped || isProcessAlive(router.pid))) {
1055
1093
  options.log?.(`Warning: runtime router (pid ${router.pid}) did not stop; continuing cleanup`);
1056
1094
  }
1057
- const helperStatusPath = join(dirname(paths.bindDir), APP_RUNTIME_HELPER_STATUS_FILE);
1058
- const helperRead = await readRuntimeHelperStatus(helperStatusPath);
1059
- let removeHelperStatus = false;
1060
- let removeHelperOwner = false;
1061
- let helperOwnerPath = null;
1062
- if (helperRead.kind === "valid") {
1095
+ // Helpers publish per-PID status files (`runtime-rotation-app-helper.<pid>.json`);
1096
+ // the un-suffixed name is the legacy shared path from before that change,
1097
+ // still checked so a pre-upgrade helper is torn down too. Every candidate
1098
+ // walks the same per-helper logic the single file used to get: stopping is
1099
+ // gated on ownership verification (status/owner identity-token agreement
1100
+ // plus process identity), so unbind reaps each helper it can prove is one
1101
+ // of ours and preserves — with a warning — anything it cannot.
1102
+ const helperBaseDir = dirname(paths.bindDir);
1103
+ let helperDirEntries = [];
1104
+ try {
1105
+ helperDirEntries = await withFileOperationRetry(() => readdir(helperBaseDir));
1106
+ }
1107
+ catch (error) {
1108
+ // Degrading to legacy-only cleanup while reporting success would leave
1109
+ // every per-PID helper running with the user told the app was unbound —
1110
+ // say so. ENOENT just means no helper ever ran.
1111
+ const code = error && typeof error === "object" && "code" in error
1112
+ ? String(error.code)
1113
+ : "unknown";
1114
+ if (code !== "ENOENT") {
1115
+ options.log?.(`Warning: could not enumerate runtime app helper status files (${code}); only the legacy helper path is checked`);
1116
+ }
1117
+ helperDirEntries = [];
1118
+ }
1119
+ const helperStatusPaths = listRuntimeHelperStatusPaths(helperBaseDir, helperDirEntries);
1120
+ // Each candidate is independent — a read, a liveness check, and at most one
1121
+ // SIGTERM/graceful-wait/SIGKILL sequence — and on the machine from #663 there
1122
+ // were 183 of them. Run them in a bounded pool rather than one after another,
1123
+ // so unbind costs roughly one stop window instead of N of them; the bound
1124
+ // keeps a machine full of stale helpers from being hit with 183 concurrent
1125
+ // signal sequences.
1126
+ const helperResults = await mapWithConcurrency(helperStatusPaths, UNBIND_HELPER_CONCURRENCY, async (helperStatusPath) => {
1127
+ const helperRead = await readRuntimeHelperStatus(helperStatusPath);
1128
+ if (helperRead.kind !== "valid")
1129
+ return null;
1063
1130
  const helper = helperRead.status;
1064
- if (helper.kind === "codex-app-runtime-rotation-helper") {
1065
- helperOwnerPath = resolveRuntimeHelperOwnerPath(dirname(paths.bindDir), helper.pid);
1066
- const helperOwner = helperOwnerPath
1067
- ? await readRuntimeHelperOwner(helperOwnerPath)
1068
- : null;
1069
- const helperOwnershipMatches = !helper.identityToken ||
1070
- (helperOwner !== null &&
1071
- helper.identityToken === helperOwner.identityToken);
1072
- if (helper.state === "running") {
1073
- if (helper.pid === null) {
1074
- options.log?.("Warning: runtime app helper status has no valid PID; preserving status");
1131
+ if (helper.kind !== "codex-app-runtime-rotation-helper")
1132
+ return null;
1133
+ let removeHelperStatus = false;
1134
+ let removeHelperOwner = false;
1135
+ const helperOwnerPath = resolveRuntimeHelperOwnerPath(helperBaseDir, helper.pid);
1136
+ const helperOwner = helperOwnerPath
1137
+ ? await readRuntimeHelperOwner(helperOwnerPath)
1138
+ : null;
1139
+ const helperOwnershipMatches = !helper.identityToken ||
1140
+ (helperOwner !== null &&
1141
+ helper.identityToken === helperOwner.identityToken);
1142
+ if (helper.state === "running") {
1143
+ if (helper.pid === null) {
1144
+ options.log?.("Warning: runtime app helper status has no valid PID; preserving status");
1145
+ }
1146
+ else {
1147
+ const wasAlive = isProcessAlive(helper.pid);
1148
+ if (!wasAlive) {
1149
+ // Decisive, and deliberately not gated on ownership (#666): a
1150
+ // dead PID means both files describe a process that no longer
1151
+ // exists, so keeping the owner file preserves nothing. It used
1152
+ // to be gated, which deleted the status file and stranded the
1153
+ // owner file — and because unbind then enumerated status paths
1154
+ // only, nothing ever rediscovered it. This matches what the
1155
+ // launcher-side sweep already does with a dead PID.
1156
+ removeHelperStatus = true;
1157
+ removeHelperOwner = helperOwnerPath !== null;
1158
+ }
1159
+ else if (!helperOwnershipMatches) {
1160
+ options.log?.("Warning: runtime app helper ownership metadata does not match; preserving status");
1075
1161
  }
1076
1162
  else {
1077
- const wasAlive = isProcessAlive(helper.pid);
1078
- if (!wasAlive) {
1079
- removeHelperStatus = true;
1080
- removeHelperOwner =
1081
- helperOwnershipMatches && helperOwnerPath !== null;
1082
- }
1083
- else if (!helperOwnershipMatches) {
1084
- options.log?.("Warning: runtime app helper ownership metadata does not match; preserving status");
1085
- }
1086
- else {
1087
- const stopped = await stopRuntimeRotationAppHelperProcess(helper, {
1088
- platform,
1089
- log: options.log,
1090
- identityToken: helper.identityToken
1091
- ? helperOwner?.identityToken
1092
- : undefined,
1093
- verifyProcessIdentity: options.verifyProcessIdentity,
1094
- });
1095
- const stillAlive = isProcessAlive(helper.pid);
1096
- removeHelperStatus = stopped && !stillAlive;
1097
- removeHelperOwner =
1098
- removeHelperStatus && helperOwnerPath !== null;
1099
- if (!removeHelperStatus) {
1100
- options.log?.(`Warning: runtime app helper (pid ${helper.pid}) did not stop; preserving status`);
1101
- }
1163
+ const stopped = await stopRuntimeRotationAppHelperProcess(helper, {
1164
+ platform,
1165
+ log: options.log,
1166
+ identityToken: helper.identityToken
1167
+ ? helperOwner?.identityToken
1168
+ : undefined,
1169
+ verifyProcessIdentity: options.verifyProcessIdentity,
1170
+ });
1171
+ const stillAlive = isProcessAlive(helper.pid);
1172
+ removeHelperStatus = stopped && !stillAlive;
1173
+ removeHelperOwner = removeHelperStatus && helperOwnerPath !== null;
1174
+ if (!removeHelperStatus) {
1175
+ options.log?.(`Warning: runtime app helper (pid ${helper.pid}) did not stop; preserving status`);
1102
1176
  }
1103
1177
  }
1104
1178
  }
1105
- else {
1106
- // A non-running, owned record is removable only when its PID is
1107
- // absent or no longer alive. This avoids deleting a status file
1108
- // while a helper is still serving despite a stale state value.
1109
- removeHelperStatus =
1110
- helper.pid === null || !isProcessAlive(helper.pid);
1111
- removeHelperOwner =
1112
- removeHelperStatus &&
1113
- helperOwnershipMatches &&
1114
- helperOwnerPath !== null;
1115
- }
1116
1179
  }
1180
+ else {
1181
+ // A non-running, owned record is removable only when its PID is
1182
+ // absent or no longer alive. This avoids deleting a status file
1183
+ // while a helper is still serving despite a stale state value.
1184
+ // Ownership does not gate the owner file here either, for the same
1185
+ // reason as above: the PID is gone, so neither file describes
1186
+ // anything that can still be running.
1187
+ removeHelperStatus = helper.pid === null || !isProcessAlive(helper.pid);
1188
+ removeHelperOwner = removeHelperStatus && helperOwnerPath !== null;
1189
+ }
1190
+ return {
1191
+ statusPath: helperStatusPath,
1192
+ ownerPath: helperOwnerPath,
1193
+ removeHelperStatus,
1194
+ removeHelperOwner,
1195
+ };
1196
+ });
1197
+ const helperCleanupPaths = [];
1198
+ // Owner paths this pass already reasoned about, whether or not it decided to
1199
+ // remove them — a preserved live helper's owner file must not then be swept
1200
+ // by the orphan pass below.
1201
+ const consideredOwnerPaths = new Set();
1202
+ for (const result of helperResults) {
1203
+ if (!result)
1204
+ continue;
1205
+ if (result.ownerPath !== null)
1206
+ consideredOwnerPaths.add(result.ownerPath);
1207
+ if (result.removeHelperStatus)
1208
+ helperCleanupPaths.push(result.statusPath);
1209
+ if (result.removeHelperOwner && result.ownerPath !== null) {
1210
+ helperCleanupPaths.push(result.ownerPath);
1211
+ }
1212
+ }
1213
+ // Owner files with no status record left to pair them with. Before #666 these
1214
+ // were unreachable: every earlier pass walked status paths only, so an owner
1215
+ // file that outlived its status file was never looked at again. A dead PID is
1216
+ // the whole test — a live PID's owner file belongs to a helper that is still
1217
+ // running, and was already considered above.
1218
+ for (const owner of listRuntimeHelperOwnerPaths(helperBaseDir, helperDirEntries)) {
1219
+ if (consideredOwnerPaths.has(owner.path))
1220
+ continue;
1221
+ if (isProcessAlive(owner.pid)) {
1222
+ // An owner file with no status record whose PID is nonetheless live
1223
+ // is the one shape this pass cannot reclaim. Either a helper is
1224
+ // starting right now and has not published yet, or — the case that
1225
+ // accumulates — the PID was recycled by an unrelated process after
1226
+ // the status file was already gone. Telling those apart needs the
1227
+ // recorded-start-time comparison the launcher-side sweep does, which
1228
+ // unbind has no equivalent of; that is a scope decision, but it
1229
+ // should not be a silent one. Every other preserve in this function
1230
+ // warns, so this one does too.
1231
+ options.log?.(`Warning: runtime app helper owner metadata (pid ${owner.pid}) has no status record but its PID is live; preserving`);
1232
+ continue;
1233
+ }
1234
+ helperCleanupPaths.push(owner.path);
1117
1235
  }
1118
1236
  await removeAppBindStartup(state ?? paths);
1119
1237
  const backup = await readAppBindBackup(paths.backupPath);
@@ -1154,8 +1272,7 @@ async function unbindCodexAppRuntimeRotationLocked(options, paths) {
1154
1272
  paths.backupPath,
1155
1273
  paths.statusPath,
1156
1274
  state?.logPath ?? paths.logPath,
1157
- ...(removeHelperStatus ? [helperStatusPath] : []),
1158
- ...(removeHelperOwner && helperOwnerPath ? [helperOwnerPath] : []),
1275
+ ...helperCleanupPaths,
1159
1276
  ];
1160
1277
  for (const candidate of cleanupCandidates) {
1161
1278
  try {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The fields any helper status record has to expose for the shared selector to
3
+ * reason about it. `rotation status` and runtime account resolution carry
4
+ * different extra fields — request counters on one side, account identity on
5
+ * the other — but they answer "which helper is current" the same way, and used
6
+ * to answer it with two hand-rolled copies that could drift apart within a
7
+ * single command (#667).
8
+ */
9
+ export interface RuntimeHelperSelectable {
10
+ state: string | null;
11
+ pid: number | null;
12
+ startedAt: number | null;
13
+ updatedAt: number | null;
14
+ }
15
+ /**
16
+ * How stale a `running` record may be before it stops counting as live.
17
+ *
18
+ * A running helper republishes its status on every tick, and the publish path
19
+ * heartbeats at least once per `APP_RUNTIME_HELPER_STATUS_HEARTBEAT_MS` (60s)
20
+ * even when nothing in the payload changed. Ten heartbeats of silence is not a
21
+ * helper that is merely quiet — it is a record whose writer is gone.
22
+ *
23
+ * This is the identity check these readers were missing. `kill(pid, 0)` answers
24
+ * "does *a* process hold this integer", so a stale record — classically the
25
+ * legacy shared `runtime-rotation-app-helper.json` left behind by a SIGKILLed
26
+ * pre-upgrade helper — passes liveness as soon as an unrelated process is
27
+ * handed its PID, and can then win selection outright. Freshness is the half of
28
+ * identity available to a synchronous reader: the wrapper verifies identity by
29
+ * probing kernel start times, but that costs a `ps` per candidate, and these
30
+ * two call sites are read-only status paths reached from the interactive menu
31
+ * as well as the CLI. Whoever holds the PID now, they are not the process that
32
+ * last wrote this file.
33
+ */
34
+ export declare const RUNTIME_HELPER_STATUS_STALE_MS: number;
35
+ /** Tolerance for clock skew between the writing helper and the reader. */
36
+ export declare const RUNTIME_HELPER_CLOCK_TOLERANCE_MS: number;
37
+ /**
38
+ * A PID is a positive integer or it is nothing. Both readers used to accept any
39
+ * finite number, so a corrupt or hand-edited record carrying `-1234` reached
40
+ * `process.kill(-1234, 0)` — which on POSIX probes process *group* 1234 and
41
+ * succeeds on any busy machine, reporting a helper that does not exist as live.
42
+ * Fractional values were accepted the same way.
43
+ */
44
+ export declare function readRuntimeHelperPid(value: unknown): number | null;
45
+ /**
46
+ * Best-effort liveness probe. `EPERM` means a process exists that this user may
47
+ * not signal, so it counts as alive; every other errno — including the `EINVAL`
48
+ * some platforms raise for a PID above their ceiling — counts as dead.
49
+ */
50
+ export declare function isRuntimeHelperProcessAlive(pid: number | null): boolean;
51
+ /**
52
+ * The single definition of "this helper is currently serving": a running state,
53
+ * a live PID, and a record recent enough to have been written by that PID's
54
+ * current occupant.
55
+ */
56
+ export declare function isLiveRuntimeHelper(status: RuntimeHelperSelectable, now?: number): boolean;
57
+ /** Every helper that passes {@link isLiveRuntimeHelper}, input order preserved. */
58
+ export declare function liveRuntimeHelpers<T extends RuntimeHelperSelectable>(statuses: readonly T[], now?: number): T[];
59
+ /**
60
+ * Prefer a live helper, and among several the most recently updated. Absent any
61
+ * live helper, fall back to the freshest record so the previous "reports the
62
+ * last helper's final state" behaviour survives.
63
+ *
64
+ * Callers that also need the live count must pass the same `statuses` array and
65
+ * the same `now` to {@link liveRuntimeHelpers}, so the selected helper and the
66
+ * count describe one instant rather than two.
67
+ */
68
+ export declare function selectRuntimeHelperStatus<T extends RuntimeHelperSelectable>(statuses: readonly T[], now?: number): T | null;
69
+ //# sourceMappingURL=app-helper-selection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-helper-selection.d.ts","sourceRoot":"","sources":["../../../lib/runtime/app-helper-selection.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,WAAW,uBAAuB;IACvC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,8BAA8B,QAAiB,CAAC;AAE7D,0EAA0E;AAC1E,eAAO,MAAM,iCAAiC,QAAY,CAAC;AAE3D;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAIlE;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAWvE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAClC,MAAM,EAAE,uBAAuB,EAC/B,GAAG,GAAE,MAAmB,GACtB,OAAO,CAgBT;AAED,mFAAmF;AACnF,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,uBAAuB,EACnE,QAAQ,EAAE,SAAS,CAAC,EAAE,EACtB,GAAG,GAAE,MAAmB,GACtB,CAAC,EAAE,CAEL;AASD;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,uBAAuB,EAC1E,QAAQ,EAAE,SAAS,CAAC,EAAE,EACtB,GAAG,GAAE,MAAmB,GACtB,CAAC,GAAG,IAAI,CAKV"}