codex-multi-auth 2.8.0 → 2.8.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 (74) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +2 -1
  3. package/dist/lib/accounts.d.ts +15 -0
  4. package/dist/lib/accounts.d.ts.map +1 -1
  5. package/dist/lib/accounts.js +126 -21
  6. package/dist/lib/codex-manager/account-pool-write.d.ts.map +1 -1
  7. package/dist/lib/codex-manager/account-pool-write.js +4 -0
  8. package/dist/lib/codex-manager/backend-settings-schema.js +1 -1
  9. package/dist/lib/codex-manager/commands/status.d.ts.map +1 -1
  10. package/dist/lib/codex-manager/commands/status.js +5 -1
  11. package/dist/lib/codex-manager/commands/uninstall.d.ts +1 -0
  12. package/dist/lib/codex-manager/commands/uninstall.d.ts.map +1 -1
  13. package/dist/lib/codex-manager/commands/uninstall.js +12 -1
  14. package/dist/lib/codex-manager/commands/why-selected.d.ts +1 -1
  15. package/dist/lib/codex-manager/commands/why-selected.d.ts.map +1 -1
  16. package/dist/lib/codex-manager/commands/why-selected.js +1 -1
  17. package/dist/lib/codex-manager/health-check.d.ts.map +1 -1
  18. package/dist/lib/codex-manager/health-check.js +19 -3
  19. package/dist/lib/codex-manager/login-oauth.d.ts.map +1 -1
  20. package/dist/lib/codex-manager/login-oauth.js +13 -7
  21. package/dist/lib/codex-manager.d.ts +4 -0
  22. package/dist/lib/codex-manager.d.ts.map +1 -1
  23. package/dist/lib/codex-manager.js +50 -16
  24. package/dist/lib/config.d.ts +3 -1
  25. package/dist/lib/config.d.ts.map +1 -1
  26. package/dist/lib/config.js +3 -1
  27. package/dist/lib/forecast.d.ts.map +1 -1
  28. package/dist/lib/forecast.js +7 -1
  29. package/dist/lib/index.d.ts +1 -0
  30. package/dist/lib/index.d.ts.map +1 -1
  31. package/dist/lib/index.js +1 -0
  32. package/dist/lib/parallel-probe.d.ts.map +1 -1
  33. package/dist/lib/parallel-probe.js +3 -1
  34. package/dist/lib/policy/runtime-policy.d.ts +1 -0
  35. package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
  36. package/dist/lib/policy/runtime-policy.js +7 -0
  37. package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -1
  38. package/dist/lib/preemptive-quota-scheduler.js +35 -15
  39. package/dist/lib/rotation.d.ts +4 -0
  40. package/dist/lib/rotation.d.ts.map +1 -1
  41. package/dist/lib/rotation.js +13 -6
  42. package/dist/lib/runtime/app-bind.d.ts +40 -0
  43. package/dist/lib/runtime/app-bind.d.ts.map +1 -1
  44. package/dist/lib/runtime/app-bind.js +550 -20
  45. package/dist/lib/runtime/rotation-account-selection.d.ts.map +1 -1
  46. package/dist/lib/runtime/rotation-account-selection.js +2 -0
  47. package/dist/lib/runtime/rotation-proxy-state.d.ts +2 -0
  48. package/dist/lib/runtime/rotation-proxy-state.d.ts.map +1 -1
  49. package/dist/lib/runtime/rotation-token-refresh.d.ts.map +1 -1
  50. package/dist/lib/runtime/rotation-token-refresh.js +3 -0
  51. package/dist/lib/runtime-constants.d.ts +2 -0
  52. package/dist/lib/runtime-constants.d.ts.map +1 -1
  53. package/dist/lib/runtime-constants.js +2 -0
  54. package/dist/lib/runtime-rotation-proxy.d.ts +8 -0
  55. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
  56. package/dist/lib/runtime-rotation-proxy.js +59 -5
  57. package/dist/lib/schemas.d.ts +15 -0
  58. package/dist/lib/schemas.d.ts.map +1 -1
  59. package/dist/lib/schemas.js +3 -0
  60. package/dist/lib/storage/flagged-storage.d.ts.map +1 -1
  61. package/dist/lib/storage/flagged-storage.js +15 -0
  62. package/dist/lib/storage/path-state.d.ts +1 -0
  63. package/dist/lib/storage/path-state.d.ts.map +1 -1
  64. package/dist/lib/storage/path-state.js +30 -3
  65. package/dist/lib/storage/public-types.d.ts +6 -0
  66. package/dist/lib/storage/public-types.d.ts.map +1 -1
  67. package/dist/lib/storage.d.ts.map +1 -1
  68. package/dist/lib/storage.js +17 -4
  69. package/package.json +7 -5
  70. package/scripts/codex-app-router.js +47 -3
  71. package/scripts/codex.js +234 -30
  72. package/scripts/install-codex-auth-utils.js +9 -2
  73. package/scripts/postinstall.js +2 -0
  74. package/scripts/preuninstall.js +11 -0
@@ -36,6 +36,7 @@ function parseArgs(argv) {
36
36
  host: "127.0.0.1",
37
37
  port: 0,
38
38
  statusPath: "",
39
+ identityToken: "",
39
40
  statePath: "",
40
41
  logPath: "",
41
42
  maxLogBytes: DEFAULT_MAX_LOG_BYTES,
@@ -58,6 +59,11 @@ function parseArgs(argv) {
58
59
  index += 1;
59
60
  continue;
60
61
  }
62
+ if (arg === "--identity-token") {
63
+ result.identityToken = next;
64
+ index += 1;
65
+ continue;
66
+ }
61
67
  if (arg === "--state") {
62
68
  result.statePath = next;
63
69
  index += 1;
@@ -133,7 +139,16 @@ function writeStatus(statusPath, payload) {
133
139
  }
134
140
  }
135
141
 
136
- function createStatusPayload({ state, proxyServer, error, stateRecord }) {
142
+ function createStatusPayload({
143
+ state,
144
+ proxyServer,
145
+ error,
146
+ stateRecord,
147
+ startedAt,
148
+ statusPath,
149
+ routerScriptPath,
150
+ identityToken,
151
+ }) {
137
152
  const proxyStatus =
138
153
  typeof proxyServer?.getStatus === "function" ? proxyServer.getStatus() : {};
139
154
  const lastAccountIndex = proxyStatus.lastAccountIndex ?? null;
@@ -149,6 +164,15 @@ function createStatusPayload({ state, proxyServer, error, stateRecord }) {
149
164
  kind: "codex-app-runtime-rotation-router",
150
165
  state,
151
166
  pid: process.pid,
167
+ startedAt,
168
+ statusPath: statusPath || readTrimmedString(stateRecord, "statusPath") || null,
169
+ identityToken:
170
+ identityToken || readTrimmedString(stateRecord, "identityToken") || null,
171
+ routerScriptPath:
172
+ routerScriptPath ||
173
+ readTrimmedString(stateRecord, "routerScriptPath") ||
174
+ process.argv[1] ||
175
+ null,
152
176
  updatedAt: Date.now(),
153
177
  baseUrl: proxyServer?.baseUrl ?? stateRecord?.baseUrl ?? null,
154
178
  totalRequests: proxyStatus.totalRequests ?? 0,
@@ -232,6 +256,7 @@ function installLogBounds(maxBytes, logPath) {
232
256
  }
233
257
 
234
258
  async function main() {
259
+ const routerStartedAt = Date.now();
235
260
  const args = parseArgs(process.argv.slice(2));
236
261
  installLogBounds(args.maxLogBytes, args.logPath).unref?.();
237
262
  const stateRecord = readState(args.statePath);
@@ -241,7 +266,16 @@ async function main() {
241
266
  );
242
267
  writeStatus(
243
268
  args.statusPath,
244
- createStatusPayload({ state: "error", proxyServer: null, error, stateRecord: null }),
269
+ createStatusPayload({
270
+ state: "error",
271
+ proxyServer: null,
272
+ error,
273
+ stateRecord: null,
274
+ startedAt: routerStartedAt,
275
+ statusPath: args.statusPath,
276
+ routerScriptPath: process.argv[1],
277
+ identityToken: args.identityToken,
278
+ }),
245
279
  );
246
280
  throw error;
247
281
  }
@@ -272,7 +306,17 @@ async function main() {
272
306
  const writeCurrentStatus = (state, error) => {
273
307
  writeStatus(
274
308
  args.statusPath || stateRecord?.statusPath || "",
275
- createStatusPayload({ state, proxyServer, error, stateRecord }),
309
+ createStatusPayload({
310
+ state,
311
+ proxyServer,
312
+ error,
313
+ stateRecord,
314
+ startedAt: routerStartedAt,
315
+ statusPath: args.statusPath || stateRecord?.statusPath || "",
316
+ routerScriptPath: stateRecord?.routerScriptPath || process.argv[1],
317
+ identityToken:
318
+ args.identityToken || stateRecord?.identityToken || "",
319
+ }),
276
320
  );
277
321
  };
278
322
 
package/scripts/codex.js CHANGED
@@ -80,6 +80,8 @@ const APP_SERVER_CONFIG_ARGS_ENV =
80
80
  "CODEX_MULTI_AUTH_APP_SERVER_CONFIG_ARGS_JSON";
81
81
  const APP_RUNTIME_HELPER_STATUS_FILE =
82
82
  RUNTIME_CONSTANTS.APP_RUNTIME_HELPER_STATUS_FILE;
83
+ const APP_RUNTIME_HELPER_OWNER_FILE =
84
+ RUNTIME_CONSTANTS.APP_RUNTIME_HELPER_OWNER_FILE;
83
85
  const DEFAULT_APP_RUNTIME_HELPER_IDLE_MS = 12 * 60 * 60 * 1000;
84
86
  const DEFAULT_APP_RUNTIME_HELPER_DETACH_GRACE_MS = 5_000;
85
87
  const APP_RUNTIME_HELPER_LAUNCH_TIMEOUT_MS = 15_000;
@@ -110,6 +112,17 @@ let shadowHomeSyncLockOwnerWriteFailuresRemaining = Number.parseInt(
110
112
  process.env.CODEX_MULTI_AUTH_TEST_SHADOW_LOCK_OWNER_WRITE_FAILURES ?? "0",
111
113
  10,
112
114
  );
115
+ let appServerShimFileCleanupBusyFailuresRemaining =
116
+ Number.parseInt(
117
+ process.env.CODEX_MULTI_AUTH_TEST_APP_SERVER_SHIM_FILE_CLEANUP_BUSY_FAILURES ??
118
+ "0",
119
+ 10,
120
+ ) || 0;
121
+ let appServerShimCopyBusyFailuresRemaining =
122
+ Number.parseInt(
123
+ process.env.CODEX_MULTI_AUTH_TEST_APP_SERVER_SHIM_COPY_BUSY_FAILURES ?? "0",
124
+ 10,
125
+ ) || 0;
113
126
  const shadowHomeCleanupRetryMarkerDir =
114
127
  (process.env.CODEX_MULTI_AUTH_TEST_SHADOW_RETRY_MARKER_DIR ?? "").trim();
115
128
  let warnedInvalidRuntimeRotationProxyEnv = false;
@@ -122,6 +135,7 @@ async function loadRuntimeConstants() {
122
135
  const fallback = {
123
136
  RUNTIME_ROTATION_PROXY_PROVIDER_ID: `${APP_SERVER_ACCOUNT_DISPLAY_NAME}-runtime-proxy`,
124
137
  APP_RUNTIME_HELPER_STATUS_FILE: "runtime-rotation-app-helper.json",
138
+ APP_RUNTIME_HELPER_OWNER_FILE: "runtime-rotation-app-helper-owner.json",
125
139
  };
126
140
  try {
127
141
  const mod = await import("../dist/lib/runtime-constants.js");
@@ -134,6 +148,10 @@ async function loadRuntimeConstants() {
134
148
  typeof mod.APP_RUNTIME_HELPER_STATUS_FILE === "string"
135
149
  ? mod.APP_RUNTIME_HELPER_STATUS_FILE
136
150
  : fallback.APP_RUNTIME_HELPER_STATUS_FILE,
151
+ APP_RUNTIME_HELPER_OWNER_FILE:
152
+ typeof mod.APP_RUNTIME_HELPER_OWNER_FILE === "string"
153
+ ? mod.APP_RUNTIME_HELPER_OWNER_FILE
154
+ : fallback.APP_RUNTIME_HELPER_OWNER_FILE,
137
155
  };
138
156
  } catch {
139
157
  // Keep wrapper startup resilient when dist has not been built yet.
@@ -171,6 +189,39 @@ function removeDirectoryWithRetry(targetPath) {
171
189
  }
172
190
  }
173
191
 
192
+ function withSynchronousFileOperationRetry(operation) {
193
+ for (
194
+ let attempt = 0;
195
+ attempt <= SHADOW_HOME_CLEANUP_BACKOFF_MS.length;
196
+ attempt += 1
197
+ ) {
198
+ try {
199
+ return operation();
200
+ } catch (error) {
201
+ if (
202
+ !isRetryableShadowHomeCleanupError(error) ||
203
+ attempt === SHADOW_HOME_CLEANUP_BACKOFF_MS.length
204
+ ) {
205
+ throw error;
206
+ }
207
+ sleepSync(SHADOW_HOME_CLEANUP_BACKOFF_MS[attempt]);
208
+ }
209
+ }
210
+ }
211
+
212
+ function maybeThrowSimulatedAppServerShimFileError(kind) {
213
+ const remaining =
214
+ kind === "copy"
215
+ ? appServerShimCopyBusyFailuresRemaining
216
+ : appServerShimFileCleanupBusyFailuresRemaining;
217
+ if (remaining <= 0) return;
218
+ if (kind === "copy") appServerShimCopyBusyFailuresRemaining -= 1;
219
+ else appServerShimFileCleanupBusyFailuresRemaining -= 1;
220
+ const error = new Error(`simulated app-server shim ${kind} EBUSY`);
221
+ error.code = "EBUSY";
222
+ throw error;
223
+ }
224
+
174
225
  /**
175
226
  * Best-effort async directory removal for hot-path callbacks (e.g. the
176
227
  * status-refresh child's close/error handlers, which fire on the PARENT event
@@ -3653,14 +3704,35 @@ function installRuntimeRotationAppServerCliShim(forwardedEnv, configArgs = []) {
3653
3704
  const preloadPath = join(shimDir, "codex-multi-auth-app-server-preload.mjs");
3654
3705
  try {
3655
3706
  try {
3656
- rmSync(executablePath, { force: true });
3707
+ withSynchronousFileOperationRetry(() => {
3708
+ maybeThrowSimulatedAppServerShimFileError("cleanup");
3709
+ rmSync(executablePath, { force: true });
3710
+ });
3657
3711
  } catch {
3658
- // Best-effort stale shim cleanup only.
3712
+ // Best-effort stale shim cleanup only; the copy below will report a
3713
+ // persistent failure without leaving a partially-created helper.
3659
3714
  }
3660
- try {
3661
- linkSync(process.execPath, executablePath);
3662
- } catch {
3663
- copyFileSync(process.execPath, executablePath);
3715
+ if (
3716
+ process.platform === "win32" ||
3717
+ (process.env.CODEX_MULTI_AUTH_TEST_FORCE_APP_SERVER_SHIM_COPY ?? "") === "1"
3718
+ ) {
3719
+ // A Windows hard link to the running node.exe remains locked by the
3720
+ // helper process itself, which prevents the shim directory from being
3721
+ // removed during graceful helper shutdown. Use an independent image so
3722
+ // the helper can clean up its app-server shim before exiting.
3723
+ withSynchronousFileOperationRetry(() => {
3724
+ maybeThrowSimulatedAppServerShimFileError("copy");
3725
+ copyFileSync(process.execPath, executablePath);
3726
+ });
3727
+ } else {
3728
+ try {
3729
+ linkSync(process.execPath, executablePath);
3730
+ } catch {
3731
+ withSynchronousFileOperationRetry(() => {
3732
+ maybeThrowSimulatedAppServerShimFileError("copy");
3733
+ copyFileSync(process.execPath, executablePath);
3734
+ });
3735
+ }
3664
3736
  }
3665
3737
  if (process.platform !== "win32") {
3666
3738
  chmodSync(executablePath, 0o755);
@@ -3704,6 +3776,19 @@ function resolveRuntimeRotationAppHelperStatusPath(env = process.env) {
3704
3776
  return join(multiAuthDir, APP_RUNTIME_HELPER_STATUS_FILE);
3705
3777
  }
3706
3778
 
3779
+ function resolveRuntimeRotationAppHelperOwnerPath(env = process.env, helperPid) {
3780
+ const multiAuthDir =
3781
+ resolveOriginalMultiAuthDir(env) ?? join(resolveCodexHomeDir(env), "multi-auth");
3782
+ const ownerFileName =
3783
+ typeof helperPid === "number" && Number.isInteger(helperPid) && helperPid > 0
3784
+ ? APP_RUNTIME_HELPER_OWNER_FILE.replace(
3785
+ /\.json$/i,
3786
+ `.${helperPid}.json`,
3787
+ )
3788
+ : APP_RUNTIME_HELPER_OWNER_FILE;
3789
+ return join(multiAuthDir, ownerFileName);
3790
+ }
3791
+
3707
3792
  function writeOwnerOnlyJsonFileAtomicSync(targetPath, payload) {
3708
3793
  const targetDir = dirname(targetPath);
3709
3794
  mkdirSync(targetDir, { recursive: true });
@@ -3816,9 +3901,38 @@ function writeRuntimeRotationAppHelperStatus(payload, env = process.env) {
3816
3901
  }
3817
3902
  }
3818
3903
 
3904
+ function writeRuntimeRotationAppHelperOwner(
3905
+ identityToken,
3906
+ helperPid,
3907
+ env = process.env,
3908
+ ) {
3909
+ if (
3910
+ typeof helperPid !== "number" ||
3911
+ !Number.isInteger(helperPid) ||
3912
+ helperPid < 1
3913
+ ) {
3914
+ return;
3915
+ }
3916
+ try {
3917
+ writeOwnerOnlyJsonFileAtomicSync(
3918
+ resolveRuntimeRotationAppHelperOwnerPath(env, helperPid),
3919
+ {
3920
+ version: 1,
3921
+ kind: "codex-app-runtime-rotation-helper-owner",
3922
+ identityToken,
3923
+ launcherPid: process.pid,
3924
+ createdAt: Date.now(),
3925
+ },
3926
+ );
3927
+ } catch {
3928
+ // Best-effort metadata; an unavailable owner file must never stop the helper.
3929
+ }
3930
+ }
3931
+
3819
3932
  function createRuntimeRotationAppHelperStatus({
3820
3933
  proxyServer,
3821
3934
  startedAt,
3935
+ identityToken,
3822
3936
  idleTimeoutMs,
3823
3937
  lastActivityAt,
3824
3938
  state,
@@ -3838,6 +3952,8 @@ function createRuntimeRotationAppHelperStatus({
3838
3952
  kind: "codex-app-runtime-rotation-helper",
3839
3953
  state,
3840
3954
  pid: process.pid,
3955
+ scriptPath: process.argv[1] ?? null,
3956
+ identityToken: identityToken || null,
3841
3957
  startedAt,
3842
3958
  updatedAt: Date.now(),
3843
3959
  baseUrl: proxyServer?.baseUrl ?? null,
@@ -3855,7 +3971,7 @@ function createRuntimeRotationAppHelperStatus({
3855
3971
  };
3856
3972
  }
3857
3973
 
3858
- async function runRuntimeRotationAppHelper() {
3974
+ async function runRuntimeRotationAppHelper(identityToken = "") {
3859
3975
  let proxyServer = null;
3860
3976
  let runtimeContext = null;
3861
3977
  let appServerShimDir = null;
@@ -3872,6 +3988,7 @@ async function runRuntimeRotationAppHelper() {
3872
3988
  createRuntimeRotationAppHelperStatus({
3873
3989
  proxyServer,
3874
3990
  startedAt,
3991
+ identityToken,
3875
3992
  idleTimeoutMs,
3876
3993
  lastActivityAt,
3877
3994
  state,
@@ -4003,6 +4120,9 @@ function waitForRuntimeRotationAppHelperExit(helper, timeoutMs = 2_000) {
4003
4120
  if (settled) return;
4004
4121
  settled = true;
4005
4122
  if (timer) clearTimeout(timer);
4123
+ // Drop the listener when the timeout wins, so a helper that outlives the
4124
+ // graceful window does not keep a live reference back into this process.
4125
+ helper.off?.("close", finish);
4006
4126
  resolve();
4007
4127
  };
4008
4128
  timer = setTimeout(finish, timeoutMs);
@@ -4010,16 +4130,49 @@ function waitForRuntimeRotationAppHelperExit(helper, timeoutMs = 2_000) {
4010
4130
  });
4011
4131
  }
4012
4132
 
4013
- function stopRuntimeRotationAppHelper(helper) {
4014
- if (!helper || helper.killed) {
4015
- return Promise.resolve();
4016
- }
4133
+ function hasRuntimeRotationAppHelperExited(helper) {
4134
+ return helper.exitCode !== null || helper.signalCode !== null;
4135
+ }
4136
+
4137
+ // Releases every handle the helper still holds in this process. The helper is
4138
+ // spawned with piped stdio, so those pipes keep the parent event loop alive on
4139
+ // their own — destroying and unref-ing them is what actually lets `mcodex` return
4140
+ // to the shell when the child ignores or outlives SIGTERM (#647).
4141
+ function releaseRuntimeRotationAppHelperResources(helper) {
4142
+ helper.stdout?.destroy();
4143
+ helper.stderr?.destroy();
4017
4144
  try {
4018
- helper.kill("SIGTERM");
4145
+ helper.unref();
4019
4146
  } catch {
4020
- return Promise.resolve();
4147
+ // Best-effort only.
4148
+ }
4149
+ }
4150
+
4151
+ async function stopRuntimeRotationAppHelper(helper) {
4152
+ if (!helper) {
4153
+ return;
4154
+ }
4155
+ if (hasRuntimeRotationAppHelperExited(helper)) {
4156
+ releaseRuntimeRotationAppHelperResources(helper);
4157
+ return;
4158
+ }
4159
+ if (!helper.killed) {
4160
+ try {
4161
+ helper.kill("SIGTERM");
4162
+ } catch {
4163
+ releaseRuntimeRotationAppHelperResources(helper);
4164
+ return;
4165
+ }
4166
+ }
4167
+ await waitForRuntimeRotationAppHelperExit(helper);
4168
+ if (!hasRuntimeRotationAppHelperExited(helper)) {
4169
+ try {
4170
+ helper.kill("SIGKILL");
4171
+ } catch {
4172
+ // Best-effort force-stop once the graceful shutdown window has elapsed.
4173
+ }
4021
4174
  }
4022
- return waitForRuntimeRotationAppHelperExit(helper);
4175
+ releaseRuntimeRotationAppHelperResources(helper);
4023
4176
  }
4024
4177
 
4025
4178
  function startRuntimeRotationAppHelper(baseContext, options = {}) {
@@ -4030,25 +4183,32 @@ function startRuntimeRotationAppHelper(baseContext, options = {}) {
4030
4183
  let stdoutBuffer = "";
4031
4184
  let stderrBuffer = "";
4032
4185
  let settled = false;
4186
+ const identityToken = randomBytes(24).toString("hex");
4187
+ const helperEnv = {
4188
+ ...baseContext.env,
4189
+ CODEX_MULTI_AUTH_DIR: resolveRuntimeRotationOriginalMultiAuthDir(
4190
+ realCodexHome,
4191
+ baseContext.env,
4192
+ ),
4193
+ [APP_RUNTIME_HELPER_OWNER_PID_ENV]: String(process.pid),
4194
+ [APP_RUNTIME_HELPER_REAL_CODEX_HOME_ENV]: realCodexHome,
4195
+ [APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV]:
4196
+ options.useCanonicalHome === true ? "1" : "0",
4197
+ };
4033
4198
  const helper = spawn(
4034
4199
  process.execPath,
4035
- [fileURLToPath(import.meta.url), INTERNAL_RUNTIME_ROTATION_APP_HELPER_ARG],
4200
+ [
4201
+ fileURLToPath(import.meta.url),
4202
+ INTERNAL_RUNTIME_ROTATION_APP_HELPER_ARG,
4203
+ identityToken,
4204
+ ],
4036
4205
  {
4037
- env: {
4038
- ...baseContext.env,
4039
- CODEX_MULTI_AUTH_DIR: resolveRuntimeRotationOriginalMultiAuthDir(
4040
- realCodexHome,
4041
- baseContext.env,
4042
- ),
4043
- [APP_RUNTIME_HELPER_OWNER_PID_ENV]: String(process.pid),
4044
- [APP_RUNTIME_HELPER_REAL_CODEX_HOME_ENV]: realCodexHome,
4045
- [APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV]:
4046
- options.useCanonicalHome === true ? "1" : "0",
4047
- },
4206
+ env: helperEnv,
4048
4207
  stdio: ["ignore", "pipe", "pipe"],
4049
4208
  detached: true,
4050
4209
  },
4051
4210
  );
4211
+ writeRuntimeRotationAppHelperOwner(identityToken, helper.pid, helperEnv);
4052
4212
  let timeout = null;
4053
4213
  const finish = (result) => {
4054
4214
  if (settled) return;
@@ -4114,6 +4274,11 @@ async function createRuntimeRotationAppHelperContext(
4114
4274
  options,
4115
4275
  );
4116
4276
  const helperEnv = message.env ?? {};
4277
+ const helperShimDir =
4278
+ typeof helperEnv.CODEX_CLI_PATH === "string" &&
4279
+ helperEnv.CODEX_CLI_PATH.length > 0
4280
+ ? helperEnv.CODEX_CLI_PATH
4281
+ : null;
4117
4282
  const helperArgs = Array.isArray(message.args)
4118
4283
  ? message.args.filter((arg) => typeof arg === "string")
4119
4284
  : [];
@@ -4127,7 +4292,22 @@ async function createRuntimeRotationAppHelperContext(
4127
4292
  helper.unref();
4128
4293
  return;
4129
4294
  }
4130
- await stopRuntimeRotationAppHelper(helper);
4295
+ try {
4296
+ await stopRuntimeRotationAppHelper(helper);
4297
+ } finally {
4298
+ // The helper normally removes its app-server shim before exiting. On
4299
+ // Windows, a descendant may still hold the copied codex.exe briefly
4300
+ // after the helper closes, so retry the parent-owned cleanup once the
4301
+ // helper shutdown path has completed as well.
4302
+ if (helperShimDir) {
4303
+ try {
4304
+ removeDirectoryWithRetry(helperShimDir);
4305
+ } catch {
4306
+ // Best-effort cleanup; stale helper directories are swept on the
4307
+ // next runtime app launch.
4308
+ }
4309
+ }
4310
+ }
4131
4311
  };
4132
4312
 
4133
4313
  return {
@@ -4171,7 +4351,10 @@ async function createRuntimeRotationProxyContextIfEnabled(
4171
4351
  if (isCodexAppCommand(rawArgs)) {
4172
4352
  return createRuntimeRotationAppHelperContext(baseContext, configTomlModule);
4173
4353
  }
4174
- if (isCodexInteractiveTuiCommand(rawArgs)) {
4354
+ if (
4355
+ isCodexInteractiveTuiCommand(rawArgs) ||
4356
+ isCodexInteractiveResumeCommand(rawArgs)
4357
+ ) {
4175
4358
  return createRuntimeRotationAppHelperContext(baseContext, configTomlModule, {
4176
4359
  detachOnExit: true,
4177
4360
  useCanonicalHome: true,
@@ -4375,6 +4558,17 @@ function isCodexInteractiveTuiCommand(rawArgs) {
4375
4558
  return findForwardedCommand(rawArgs) === null;
4376
4559
  }
4377
4560
 
4561
+ // `resume` and `fork` are interactive TUI entry points that happen to carry a
4562
+ // forwarded subcommand, so the bare-invocation predicate above misses them. They
4563
+ // must not take the shadow-home transport: the shadow mirror deliberately omits the
4564
+ // runtime SQLite state (`isRuntimeRotationShadowHomeOmittedEntry`), so the requested
4565
+ // thread is absent from the shadow session index and the resumed TUI hangs on a
4566
+ // blank screen (#647).
4567
+ function isCodexInteractiveResumeCommand(rawArgs) {
4568
+ const command = findForwardedCommand(rawArgs)?.command;
4569
+ return command === "resume" || command === "fork";
4570
+ }
4571
+
4378
4572
  function shouldUseRuntimeRoutingForForwardedArgs(rawArgs) {
4379
4573
  if (!Array.isArray(rawArgs) || rawArgs.length === 0) {
4380
4574
  return true;
@@ -4414,7 +4608,17 @@ function shouldUseRuntimeRoutingForForwardedArgs(rawArgs) {
4414
4608
  );
4415
4609
  }
4416
4610
 
4417
- if (command.command === "app" && hasHelpFlagAfterCommand(rawArgs, command.index)) {
4611
+ // Request commands still print their help without making a single model
4612
+ // request, so the help form skips the transport entirely: no proxy, no shadow
4613
+ // home, no detached helper. This matters most for the interactive commands,
4614
+ // which detach their helper on a clean exit — help always exits clean, so
4615
+ // `resume --help` would otherwise strand a helper until its idle timeout. It is
4616
+ // keyed off the help flag rather than the command, so a real run still routes
4617
+ // through rotation, and it matches how `app-server` help is handled above (#647).
4618
+ if (
4619
+ requestCommands.has(command.command) &&
4620
+ hasHelpFlagAfterCommand(rawArgs, command.index)
4621
+ ) {
4418
4622
  return false;
4419
4623
  }
4420
4624
  if (requestCommands.has(command.command)) {
@@ -5106,7 +5310,7 @@ async function main() {
5106
5310
 
5107
5311
  const rawArgs = process.argv.slice(2);
5108
5312
  if (rawArgs[0] === INTERNAL_RUNTIME_ROTATION_APP_HELPER_ARG) {
5109
- return runRuntimeRotationAppHelper();
5313
+ return runRuntimeRotationAppHelper(rawArgs[1] ?? "");
5110
5314
  }
5111
5315
 
5112
5316
  const normalizedArgs = normalizeAuthAlias(rawArgs);
@@ -6,6 +6,8 @@ import { join } from "node:path";
6
6
  import process from "node:process";
7
7
 
8
8
  const PLUGIN_NAME = "codex-multi-auth";
9
+ const LEGACY_PLUGIN_NAME = "@ndycode/codex-multi-auth";
10
+ const PLUGIN_NAMES = [PLUGIN_NAME, LEGACY_PLUGIN_NAME];
9
11
 
10
12
  const TRUE_VALUES = new Set(["1", "true", "yes"]);
11
13
  const FALSE_VALUES = new Set(["0", "false", "no"]);
@@ -82,6 +84,7 @@ export function resolveInstallPaths(
82
84
  configPath,
83
85
  cacheDir,
84
86
  cacheNodeModules: join(cacheDir, "node_modules", PLUGIN_NAME),
87
+ cacheLegacyNodeModules: join(cacheDir, "node_modules", "@ndycode", "codex-multi-auth"),
85
88
  cacheBunLock: join(cacheDir, "bun.lock"),
86
89
  cachePackageJson: join(cacheDir, "package.json"),
87
90
  };
@@ -92,7 +95,9 @@ export function removePluginFromList(list) {
92
95
  const entries = Array.isArray(list) ? list.filter(Boolean) : [];
93
96
  return entries.filter((entry) => {
94
97
  if (typeof entry !== "string") return true;
95
- return entry !== PLUGIN_NAME && !entry.startsWith(`${PLUGIN_NAME}@`);
98
+ return !PLUGIN_NAMES.some(
99
+ (pluginName) => entry === pluginName || entry.startsWith(`${pluginName}@`),
100
+ );
96
101
  });
97
102
  }
98
103
 
@@ -101,7 +106,9 @@ export function normalizePluginList(list) {
101
106
  const entries = Array.isArray(list) ? list.filter(Boolean) : [];
102
107
  const filtered = entries.filter((entry) => {
103
108
  if (typeof entry !== "string") return true;
104
- return entry !== PLUGIN_NAME && !entry.startsWith(`${PLUGIN_NAME}@`);
109
+ return !PLUGIN_NAMES.some(
110
+ (pluginName) => entry === pluginName || entry.startsWith(`${pluginName}@`),
111
+ );
105
112
  });
106
113
  const deduped = [];
107
114
  const seen = new Set();
@@ -33,6 +33,8 @@ const CI_ENV_KEYS = [
33
33
  export const INSTALL_NOTICE = [
34
34
  "codex-multi-auth installed. Run `codex-multi-auth --help` to get started.",
35
35
  "App integration (Codex app bind + launcher shortcuts) completes automatically on first run.",
36
+ "To remove it later, run `codex-multi-auth uninstall`, then `npm uninstall -g codex-multi-auth`.",
37
+ "`@ndycode/codex-multi-auth` is the legacy scoped package name; it does not remove this package.",
36
38
  ].join("\n");
37
39
 
38
40
  /**
@@ -189,6 +189,9 @@ export async function runPreuninstallCleanup(deps = {}) {
189
189
  );
190
190
  if (dryRun) {
191
191
  log(`[dry-run] Would remove ${paths.cacheNodeModules}`);
192
+ if (paths.cacheLegacyNodeModules) {
193
+ log(`[dry-run] Would remove ${paths.cacheLegacyNodeModules}`);
194
+ }
192
195
  if (bunLockSafe) {
193
196
  log(`[dry-run] Would remove ${paths.cacheBunLock}`);
194
197
  } else {
@@ -201,6 +204,14 @@ export async function runPreuninstallCleanup(deps = {}) {
201
204
  await withFileOperationRetry(() =>
202
205
  rm(paths.cacheNodeModules, { recursive: true, force: true }),
203
206
  );
207
+ if (paths.cacheLegacyNodeModules) {
208
+ await withFileOperationRetry(() =>
209
+ rm(paths.cacheLegacyNodeModules, {
210
+ recursive: true,
211
+ force: true,
212
+ }),
213
+ );
214
+ }
204
215
  if (bunLockSafe) {
205
216
  await withFileOperationRetry(() =>
206
217
  rm(paths.cacheBunLock, { force: true }),