codex-multi-auth 2.8.4 → 2.8.6

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 (44) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +4 -4
  3. package/dist/lib/accounts.d.ts +7 -0
  4. package/dist/lib/accounts.d.ts.map +1 -1
  5. package/dist/lib/accounts.js +10 -0
  6. package/dist/lib/auth/auth.d.ts.map +1 -1
  7. package/dist/lib/auth/auth.js +8 -3
  8. package/dist/lib/codex-manager/commands/best.d.ts +3 -0
  9. package/dist/lib/codex-manager/commands/best.d.ts.map +1 -1
  10. package/dist/lib/codex-manager/commands/best.js +10 -1
  11. package/dist/lib/codex-manager/commands/forecast.d.ts +3 -0
  12. package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
  13. package/dist/lib/codex-manager/commands/forecast.js +18 -1
  14. package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
  15. package/dist/lib/codex-manager/commands/report.js +14 -0
  16. package/dist/lib/codex-manager/commands/rotation.d.ts.map +1 -1
  17. package/dist/lib/codex-manager/commands/rotation.js +43 -20
  18. package/dist/lib/forecast.d.ts +13 -0
  19. package/dist/lib/forecast.d.ts.map +1 -1
  20. package/dist/lib/forecast.js +18 -3
  21. package/dist/lib/request/rate-limit-decision.d.ts +19 -1
  22. package/dist/lib/request/rate-limit-decision.d.ts.map +1 -1
  23. package/dist/lib/request/rate-limit-decision.js +34 -2
  24. package/dist/lib/runtime/account-status.d.ts +38 -0
  25. package/dist/lib/runtime/account-status.d.ts.map +1 -1
  26. package/dist/lib/runtime/account-status.js +68 -0
  27. package/dist/lib/runtime/app-bind.d.ts +7 -0
  28. package/dist/lib/runtime/app-bind.d.ts.map +1 -1
  29. package/dist/lib/runtime/app-bind.js +174 -57
  30. package/dist/lib/runtime/app-helper-selection.d.ts +69 -0
  31. package/dist/lib/runtime/app-helper-selection.d.ts.map +1 -0
  32. package/dist/lib/runtime/app-helper-selection.js +100 -0
  33. package/dist/lib/runtime/rotation-server-types.d.ts +7 -0
  34. package/dist/lib/runtime/rotation-server-types.d.ts.map +1 -1
  35. package/dist/lib/runtime/runtime-current-account.d.ts +3 -2
  36. package/dist/lib/runtime/runtime-current-account.d.ts.map +1 -1
  37. package/dist/lib/runtime/runtime-current-account.js +34 -26
  38. package/dist/lib/runtime-constants.d.ts +30 -0
  39. package/dist/lib/runtime-constants.d.ts.map +1 -1
  40. package/dist/lib/runtime-constants.js +53 -0
  41. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
  42. package/dist/lib/runtime-rotation-proxy.js +72 -2
  43. package/package.json +1 -2
  44. package/scripts/codex.js +601 -49
@@ -0,0 +1,100 @@
1
+ import process from "node:process";
2
+ /**
3
+ * How stale a `running` record may be before it stops counting as live.
4
+ *
5
+ * A running helper republishes its status on every tick, and the publish path
6
+ * heartbeats at least once per `APP_RUNTIME_HELPER_STATUS_HEARTBEAT_MS` (60s)
7
+ * even when nothing in the payload changed. Ten heartbeats of silence is not a
8
+ * helper that is merely quiet — it is a record whose writer is gone.
9
+ *
10
+ * This is the identity check these readers were missing. `kill(pid, 0)` answers
11
+ * "does *a* process hold this integer", so a stale record — classically the
12
+ * legacy shared `runtime-rotation-app-helper.json` left behind by a SIGKILLed
13
+ * pre-upgrade helper — passes liveness as soon as an unrelated process is
14
+ * handed its PID, and can then win selection outright. Freshness is the half of
15
+ * identity available to a synchronous reader: the wrapper verifies identity by
16
+ * probing kernel start times, but that costs a `ps` per candidate, and these
17
+ * two call sites are read-only status paths reached from the interactive menu
18
+ * as well as the CLI. Whoever holds the PID now, they are not the process that
19
+ * last wrote this file.
20
+ */
21
+ export const RUNTIME_HELPER_STATUS_STALE_MS = 10 * 60 * 1000;
22
+ /** Tolerance for clock skew between the writing helper and the reader. */
23
+ export const RUNTIME_HELPER_CLOCK_TOLERANCE_MS = 60 * 1000;
24
+ /**
25
+ * A PID is a positive integer or it is nothing. Both readers used to accept any
26
+ * finite number, so a corrupt or hand-edited record carrying `-1234` reached
27
+ * `process.kill(-1234, 0)` — which on POSIX probes process *group* 1234 and
28
+ * succeeds on any busy machine, reporting a helper that does not exist as live.
29
+ * Fractional values were accepted the same way.
30
+ */
31
+ export function readRuntimeHelperPid(value) {
32
+ return typeof value === "number" && Number.isInteger(value) && value > 0
33
+ ? value
34
+ : null;
35
+ }
36
+ /**
37
+ * Best-effort liveness probe. `EPERM` means a process exists that this user may
38
+ * not signal, so it counts as alive; every other errno — including the `EINVAL`
39
+ * some platforms raise for a PID above their ceiling — counts as dead.
40
+ */
41
+ export function isRuntimeHelperProcessAlive(pid) {
42
+ const probePid = readRuntimeHelperPid(pid);
43
+ if (probePid === null)
44
+ return false;
45
+ try {
46
+ process.kill(probePid, 0);
47
+ return true;
48
+ }
49
+ catch (error) {
50
+ const code = error && typeof error === "object" && "code" in error ? error.code : null;
51
+ return code === "EPERM";
52
+ }
53
+ }
54
+ /**
55
+ * The single definition of "this helper is currently serving": a running state,
56
+ * a live PID, and a record recent enough to have been written by that PID's
57
+ * current occupant.
58
+ */
59
+ export function isLiveRuntimeHelper(status, now = Date.now()) {
60
+ if (status.state !== "running")
61
+ return false;
62
+ if (!isRuntimeHelperProcessAlive(status.pid))
63
+ return false;
64
+ // A record that claims to have started after the current instant was not
65
+ // written by a process that is running now.
66
+ if (status.startedAt !== null &&
67
+ status.startedAt > now + RUNTIME_HELPER_CLOCK_TOLERANCE_MS) {
68
+ return false;
69
+ }
70
+ // A record with no `updatedAt` at all predates the heartbeat contract and
71
+ // cannot be judged on freshness; it falls back to bare liveness rather than
72
+ // being discarded, which is no worse than the behaviour it replaces.
73
+ if (status.updatedAt === null)
74
+ return true;
75
+ return now - status.updatedAt <= RUNTIME_HELPER_STATUS_STALE_MS;
76
+ }
77
+ /** Every helper that passes {@link isLiveRuntimeHelper}, input order preserved. */
78
+ export function liveRuntimeHelpers(statuses, now = Date.now()) {
79
+ return statuses.filter((status) => isLiveRuntimeHelper(status, now));
80
+ }
81
+ function byRecency(left, right) {
82
+ return (right.updatedAt ?? 0) - (left.updatedAt ?? 0);
83
+ }
84
+ /**
85
+ * Prefer a live helper, and among several the most recently updated. Absent any
86
+ * live helper, fall back to the freshest record so the previous "reports the
87
+ * last helper's final state" behaviour survives.
88
+ *
89
+ * Callers that also need the live count must pass the same `statuses` array and
90
+ * the same `now` to {@link liveRuntimeHelpers}, so the selected helper and the
91
+ * count describe one instant rather than two.
92
+ */
93
+ export function selectRuntimeHelperStatus(statuses, now = Date.now()) {
94
+ if (statuses.length === 0)
95
+ return null;
96
+ const live = liveRuntimeHelpers(statuses, now).sort(byRecency);
97
+ if (live.length > 0)
98
+ return live[0] ?? null;
99
+ return [...statuses].sort(byRecency)[0] ?? null;
100
+ }
@@ -6,6 +6,13 @@ export interface RuntimeRotationProxyServer {
6
6
  baseUrl: string;
7
7
  close: () => Promise<void>;
8
8
  getStatus: () => RuntimeRotationProxyStatus;
9
+ /**
10
+ * Number of client sockets currently open against the proxy. The detached
11
+ * app helper uses it to tell a handed-off consumer from a stranded process;
12
+ * optional so a proxy shape without it degrades to activity-only accounting
13
+ * rather than failing to start.
14
+ */
15
+ getOpenConnectionCount?: () => number;
9
16
  }
10
17
  export interface RuntimeRotationProxyStatus {
11
18
  startedAt: number;
@@ -1 +1 @@
1
- {"version":3,"file":"rotation-server-types.d.ts","sourceRoot":"","sources":["../../../lib/runtime/rotation-server-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,WAAW,0BAA0B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,SAAS,EAAE,MAAM,0BAA0B,CAAC;CAC5C;AAED,MAAM,WAAW,0BAA0B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,2BAA2B;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,8BAA8B,CAAC,EAAE,MAAM,CAAC;IACxC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,MAAM,gBAAgB,GACzB,YAAY,GACZ,cAAc,GACd,eAAe,GACf,cAAc,GACd,QAAQ,GACR,aAAa,GACb,YAAY,CAAC;AAChB,MAAM,MAAM,qBAAqB,GAAG,KAAK,GAAG;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,WAAW,8BAA8B;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;CAClB"}
1
+ {"version":3,"file":"rotation-server-types.d.ts","sourceRoot":"","sources":["../../../lib/runtime/rotation-server-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,WAAW,0BAA0B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,SAAS,EAAE,MAAM,0BAA0B,CAAC;IAC5C;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,MAAM,MAAM,CAAC;CACtC;AAED,MAAM,WAAW,0BAA0B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,2BAA2B;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,8BAA8B,CAAC,EAAE,MAAM,CAAC;IACxC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,MAAM,gBAAgB,GACzB,YAAY,GACZ,cAAc,GACd,eAAe,GACf,cAAc,GACd,QAAQ,GACR,aAAa,GACb,YAAY,CAAC;AAChB,MAAM,MAAM,qBAAqB,GAAG,KAAK,GAAG;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,WAAW,8BAA8B;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;CAClB"}
@@ -35,6 +35,7 @@ export interface AppRuntimeHelperAccountStatus {
35
35
  kind: string | null;
36
36
  state: string | null;
37
37
  pid: number | null;
38
+ startedAt: number | null;
38
39
  lastAccountIndex: number | null;
39
40
  lastAccountLabel: string | null;
40
41
  lastAccountEmail: string | null;
@@ -42,8 +43,8 @@ export interface AppRuntimeHelperAccountStatus {
42
43
  lastAccountUpdatedAt: number | null;
43
44
  updatedAt: number | null;
44
45
  }
45
- export declare function readAppRuntimeHelperStatus(): AppRuntimeHelperAccountStatus | null;
46
- export declare function appRuntimeHelperStatusToSignal(status: AppRuntimeHelperAccountStatus | null): RuntimeAccountSignal | null;
46
+ export declare function readAppRuntimeHelperStatus(now?: number): AppRuntimeHelperAccountStatus | null;
47
+ export declare function appRuntimeHelperStatusToSignal(status: AppRuntimeHelperAccountStatus | null, now?: number): RuntimeAccountSignal | null;
47
48
  export declare function readAppRuntimeHelperAccountSignal(): RuntimeAccountSignal | null;
48
49
  export declare function resolveRuntimeCurrentAccount(storage: AccountStorageV3, sources: RuntimeCurrentAccountSources, options?: RuntimeCurrentAccountOptions): RuntimeCurrentAccountSelection | null;
49
50
  export declare function resolveAccountCurrentMarkers(index: number, storedCurrentIndex: number, runtimeCurrent: RuntimeCurrentAccountSelection | null): AccountCurrentMarker[];
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-current-account.d.ts","sourceRoot":"","sources":["../../../lib/runtime/runtime-current-account.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAC/E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGtD,KAAK,2BAA2B,GAC7B,uBAAuB,GACvB,UAAU,GACV,YAAY,CAAC;AAEhB,KAAK,0BAA0B,GAAG,YAAY,GAAG,OAAO,GAAG,OAAO,CAAC;AAEnE,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;AAErE,MAAM,WAAW,oBAAoB;IACpC,MAAM,EAAE,2BAA2B,CAAC;IACpC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,8BAA8B;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,2BAA2B,CAAC;IACpC,SAAS,EAAE,0BAA0B,CAAC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,4BAA4B;IAC5C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,4BAA4B;IAC5C,eAAe,CAAC,EAAE,4BAA4B,GAAG,IAAI,CAAC;IACtD,aAAa,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC3C,eAAe,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;CAC9C;AAMD,MAAM,WAAW,6BAA6B;IAC7C,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AA8DD,wBAAgB,0BAA0B,IAAI,6BAA6B,GAAG,IAAI,CA0BjF;AAED,wBAAgB,8BAA8B,CAC7C,MAAM,EAAE,6BAA6B,GAAG,IAAI,GAC1C,oBAAoB,GAAG,IAAI,CAc7B;AAED,wBAAgB,iCAAiC,IAAI,oBAAoB,GAAG,IAAI,CAE/E;AA+FD,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,4BAA4B,EACrC,OAAO,GAAE,4BAAiC,GACxC,8BAA8B,GAAG,IAAI,CAgDvC;AAED,wBAAgB,4BAA4B,CAC3C,KAAK,EAAE,MAAM,EACb,kBAAkB,EAAE,MAAM,EAC1B,cAAc,EAAE,8BAA8B,GAAG,IAAI,GACnD,oBAAoB,EAAE,CAWxB;AAED,wBAAgB,uBAAuB,CACtC,KAAK,EAAE,MAAM,EACb,kBAAkB,EAAE,MAAM,EAC1B,cAAc,EAAE,8BAA8B,GAAG,IAAI,GACnD,OAAO,CAET"}
1
+ {"version":3,"file":"runtime-current-account.d.ts","sourceRoot":"","sources":["../../../lib/runtime/runtime-current-account.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,6 @@
1
- import { existsSync, readFileSync, statSync } from "node:fs";
2
- import { join } from "node:path";
3
- import process from "node:process";
4
- import { APP_RUNTIME_HELPER_STATUS_FILE } from "../runtime-constants.js";
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { isLiveRuntimeHelper, readRuntimeHelperPid, selectRuntimeHelperStatus, } from "./app-helper-selection.js";
3
+ import { listRuntimeHelperStatusPaths } from "../runtime-constants.js";
5
4
  import { getCodexMultiAuthDir } from "../runtime-paths.js";
6
5
  import { isRecord } from "../utils.js";
7
6
  const RUNTIME_CURRENT_ACCOUNT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
@@ -46,22 +45,7 @@ function readOptionalString(record, key) {
46
45
  ? value.trim()
47
46
  : null;
48
47
  }
49
- // Best-effort liveness probe: process.kill(pid, 0) can report permission
50
- // failures for live processes and cannot protect against rare PID reuse.
51
- function isProcessAlive(pid) {
52
- if (!pid)
53
- return false;
54
- try {
55
- process.kill(pid, 0);
56
- return true;
57
- }
58
- catch (error) {
59
- const code = error && typeof error === "object" && "code" in error ? error.code : null;
60
- return code === "EPERM";
61
- }
62
- }
63
- export function readAppRuntimeHelperStatus() {
64
- const statusPath = join(getCodexMultiAuthDir(), APP_RUNTIME_HELPER_STATUS_FILE);
48
+ function readAppRuntimeHelperStatusFile(statusPath) {
65
49
  if (!existsSync(statusPath))
66
50
  return null;
67
51
  try {
@@ -79,7 +63,8 @@ export function readAppRuntimeHelperStatus() {
79
63
  return {
80
64
  kind: readOptionalString(parsed, "kind"),
81
65
  state: readOptionalString(parsed, "state"),
82
- pid: readOptionalNumber(parsed, "pid"),
66
+ pid: readRuntimeHelperPid(parsed.pid),
67
+ startedAt: readOptionalNumber(parsed, "startedAt"),
83
68
  lastAccountIndex: readOptionalNumber(parsed, "lastAccountIndex"),
84
69
  lastAccountLabel: readOptionalString(parsed, "lastAccountLabel"),
85
70
  lastAccountEmail: readOptionalString(parsed, "lastAccountEmail"),
@@ -92,14 +77,34 @@ export function readAppRuntimeHelperStatus() {
92
77
  return null;
93
78
  }
94
79
  }
95
- export function appRuntimeHelperStatusToSignal(status) {
80
+ // Helpers publish per-PID status files (`runtime-rotation-app-helper.<pid>.json`)
81
+ // so N concurrent helpers stop overwriting one shared path; path discovery is
82
+ // shared with every other reader via listRuntimeHelperStatusPaths.
83
+ function listAppRuntimeHelperStatusPaths(multiAuthDir) {
84
+ let entries = [];
85
+ try {
86
+ entries = readdirSync(multiAuthDir);
87
+ }
88
+ catch {
89
+ entries = [];
90
+ }
91
+ return listRuntimeHelperStatusPaths(multiAuthDir, entries);
92
+ }
93
+ export function readAppRuntimeHelperStatus(now = Date.now()) {
94
+ const statuses = listAppRuntimeHelperStatusPaths(getCodexMultiAuthDir())
95
+ .map(readAppRuntimeHelperStatusFile)
96
+ .filter((status) => status !== null && status.kind === APP_RUNTIME_HELPER_KIND);
97
+ // Selection is shared with `rotation status` so the helper named on the
98
+ // status line and the helper whose account is marked `current` can never be
99
+ // two different helpers (#667).
100
+ return selectRuntimeHelperStatus(statuses, now);
101
+ }
102
+ export function appRuntimeHelperStatusToSignal(status, now = Date.now()) {
96
103
  if (!status)
97
104
  return null;
98
105
  if (status.kind !== APP_RUNTIME_HELPER_KIND)
99
106
  return null;
100
- if (status.state !== "running")
101
- return null;
102
- if (!isProcessAlive(status.pid))
107
+ if (!isLiveRuntimeHelper(status, now))
103
108
  return null;
104
109
  return {
105
110
  source: "app-helper",
@@ -112,7 +117,10 @@ export function appRuntimeHelperStatusToSignal(status) {
112
117
  };
113
118
  }
114
119
  export function readAppRuntimeHelperAccountSignal() {
115
- return appRuntimeHelperStatusToSignal(readAppRuntimeHelperStatus());
120
+ // One `now` for both the selection and the liveness verdict, so a helper
121
+ // cannot be selected against one instant and judged against another.
122
+ const now = Date.now();
123
+ return appRuntimeHelperStatusToSignal(readAppRuntimeHelperStatus(now), now);
116
124
  }
117
125
  function runtimeSnapshotToSignal(snapshot) {
118
126
  if (!snapshot)
@@ -2,4 +2,34 @@ export declare const RUNTIME_ROTATION_PROXY_PROVIDER_ID: "codex-multi-auth-runti
2
2
  export declare const APP_RUNTIME_HELPER_STATUS_FILE: "runtime-rotation-app-helper.json";
3
3
  /** Immutable launcher metadata used to verify ownership before stopping a helper. */
4
4
  export declare const APP_RUNTIME_HELPER_OWNER_FILE: "runtime-rotation-app-helper-owner.json";
5
+ /**
6
+ * The one place the per-PID filename shape is written down:
7
+ * `<base-without-.json>.<pid>.json`, matched case-insensitively with the PID
8
+ * captured. Status files and owner files share that shape, and so does the
9
+ * launcher-side sweep in `scripts/codex.js` — that file re-derives the pattern
10
+ * from the same two constants because it has to keep working before `dist/` is
11
+ * built, so a change to the shape here is a change there too.
12
+ */
13
+ export declare function runtimeHelperPerPidPattern(baseName: string): RegExp;
14
+ /**
15
+ * Every path a helper status record can live at: the per-PID files
16
+ * (`runtime-rotation-app-helper.<pid>.json`, one per helper) plus the
17
+ * un-suffixed legacy shared path from before the per-PID change, which is
18
+ * still read so a pre-upgrade helper stays visible. The filename contract
19
+ * lives here, next to the constant it derives from, so every reader agrees
20
+ * on it; callers supply the directory listing so this stays pure and their
21
+ * own error handling for the `readdir` stays theirs.
22
+ */
23
+ export declare function listRuntimeHelperStatusPaths(baseDir: string, entries: readonly string[]): string[];
24
+ /**
25
+ * Owner files paired with the helper PID they belong to. Unbind used to
26
+ * enumerate status files only, so an owner file whose status record had
27
+ * already been removed could never be rediscovered and accumulated under the
28
+ * multi-auth root forever (#666). Enumerating both is what lets a cleanup pass
29
+ * reclaim an owner file that outlived its status record.
30
+ */
31
+ export declare function listRuntimeHelperOwnerPaths(baseDir: string, entries: readonly string[]): {
32
+ path: string;
33
+ pid: number;
34
+ }[];
5
35
  //# sourceMappingURL=runtime-constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-constants.d.ts","sourceRoot":"","sources":["../../lib/runtime-constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kCAAkC,EAC9C,gCAAyC,CAAC;AAE3C,eAAO,MAAM,8BAA8B,EAC1C,kCAA2C,CAAC;AAE7C,qFAAqF;AACrF,eAAO,MAAM,6BAA6B,EACzC,wCAAiD,CAAC"}
1
+ {"version":3,"file":"runtime-constants.d.ts","sourceRoot":"","sources":["../../lib/runtime-constants.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,kCAAkC,EAC9C,gCAAyC,CAAC;AAE3C,eAAO,MAAM,8BAA8B,EAC1C,kCAA2C,CAAC;AAE7C,qFAAqF;AACrF,eAAO,MAAM,6BAA6B,EACzC,wCAAiD,CAAC;AAEnD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMnE;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,SAAS,MAAM,EAAE,GACxB,MAAM,EAAE,CAUV;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAC1C,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,SAAS,MAAM,EAAE,GACxB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAAE,CAcjC"}
@@ -1,4 +1,57 @@
1
+ import { join } from "node:path";
1
2
  export const RUNTIME_ROTATION_PROXY_PROVIDER_ID = "codex-multi-auth-runtime-proxy";
2
3
  export const APP_RUNTIME_HELPER_STATUS_FILE = "runtime-rotation-app-helper.json";
3
4
  /** Immutable launcher metadata used to verify ownership before stopping a helper. */
4
5
  export const APP_RUNTIME_HELPER_OWNER_FILE = "runtime-rotation-app-helper-owner.json";
6
+ /**
7
+ * The one place the per-PID filename shape is written down:
8
+ * `<base-without-.json>.<pid>.json`, matched case-insensitively with the PID
9
+ * captured. Status files and owner files share that shape, and so does the
10
+ * launcher-side sweep in `scripts/codex.js` — that file re-derives the pattern
11
+ * from the same two constants because it has to keep working before `dist/` is
12
+ * built, so a change to the shape here is a change there too.
13
+ */
14
+ export function runtimeHelperPerPidPattern(baseName) {
15
+ const prefix = baseName.replace(/\.json$/i, "");
16
+ return new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.(\\d+)\\.json$`, "i");
17
+ }
18
+ /**
19
+ * Every path a helper status record can live at: the per-PID files
20
+ * (`runtime-rotation-app-helper.<pid>.json`, one per helper) plus the
21
+ * un-suffixed legacy shared path from before the per-PID change, which is
22
+ * still read so a pre-upgrade helper stays visible. The filename contract
23
+ * lives here, next to the constant it derives from, so every reader agrees
24
+ * on it; callers supply the directory listing so this stays pure and their
25
+ * own error handling for the `readdir` stays theirs.
26
+ */
27
+ export function listRuntimeHelperStatusPaths(baseDir, entries) {
28
+ const perPidPattern = runtimeHelperPerPidPattern(APP_RUNTIME_HELPER_STATUS_FILE);
29
+ return [
30
+ ...entries
31
+ .filter((name) => perPidPattern.test(name))
32
+ .map((name) => join(baseDir, name)),
33
+ join(baseDir, APP_RUNTIME_HELPER_STATUS_FILE),
34
+ ];
35
+ }
36
+ /**
37
+ * Owner files paired with the helper PID they belong to. Unbind used to
38
+ * enumerate status files only, so an owner file whose status record had
39
+ * already been removed could never be rediscovered and accumulated under the
40
+ * multi-auth root forever (#666). Enumerating both is what lets a cleanup pass
41
+ * reclaim an owner file that outlived its status record.
42
+ */
43
+ export function listRuntimeHelperOwnerPaths(baseDir, entries) {
44
+ const perPidPattern = runtimeHelperPerPidPattern(APP_RUNTIME_HELPER_OWNER_FILE);
45
+ const owners = [];
46
+ for (const name of entries) {
47
+ const match = perPidPattern.exec(name);
48
+ const captured = match?.[1];
49
+ if (captured === undefined)
50
+ continue;
51
+ const pid = Number.parseInt(captured, 10);
52
+ if (!Number.isInteger(pid) || pid < 1)
53
+ continue;
54
+ owners.push({ path: join(baseDir, name), pid });
55
+ }
56
+ return owners;
57
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-rotation-proxy.d.ts","sourceRoot":"","sources":["../../lib/runtime-rotation-proxy.ts"],"names":[],"mappings":"AAGA,OAAO,EAGN,KAAK,cAAc,EACnB,MAAM,eAAe,CAAC;AA8BvB,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA2CtE,OAAO,KAAK,EAKX,2BAA2B,EAC3B,0BAA0B,EAE1B,MAAM,oCAAoC,CAAC;AAe5C,YAAY,EACX,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,GAC1B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACN,+BAA+B,EAC/B,0BAA0B,GAC1B,MAAM,kCAAkC,CAAC;AAC1C,YAAY,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AACxE,OAAO,EACN,+BAA+B,EAC/B,8BAA8B,EAC9B,uBAAuB,EACvB,uBAAuB,GACvB,MAAM,oCAAoC,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AAiDtE,kFAAkF;AAClF,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,GAAG;IACvE,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,EACD,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,MAAM,CAmBR;AA8bD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAC1C,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,MAAM,GAAG,IAAI,CAUf;AAED,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,2BAA2B,GAClC,OAAO,CAAC,0BAA0B,CAAC,CAwKrC"}
1
+ {"version":3,"file":"runtime-rotation-proxy.d.ts","sourceRoot":"","sources":["../../lib/runtime-rotation-proxy.ts"],"names":[],"mappings":"AAGA,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,eAAe,CAAC;AA8BvB,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA4CtE,OAAO,KAAK,EAKX,2BAA2B,EAC3B,0BAA0B,EAE1B,MAAM,oCAAoC,CAAC;AAe5C,YAAY,EACX,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,GAC1B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACN,+BAA+B,EAC/B,0BAA0B,GAC1B,MAAM,kCAAkC,CAAC;AAC1C,YAAY,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AACxE,OAAO,EACN,+BAA+B,EAC/B,8BAA8B,EAC9B,uBAAuB,EACvB,uBAAuB,GACvB,MAAM,oCAAoC,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AA8DtE,kFAAkF;AAClF,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,GAAG;IACvE,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,EACD,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,MAAM,CAmBR;AA8bD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAC1C,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,MAAM,GAAG,IAAI,CAUf;AAED,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,2BAA2B,GAClC,OAAO,CAAC,0BAA0B,CAAC,CA4KrC"}
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
- import { AccountManager, extractAccountId, } from "./accounts.js";
3
+ import { AccountManager, AUTH_INVALIDATION_MARKER, extractAccountId, } from "./accounts.js";
4
4
  import { withRoutingMutex } from "./routing-mutex.js";
5
5
  import { getFetchTimeoutMs, getNetworkErrorCooldownMs, getRetryAllAccountsMaxRetries, getServerErrorCooldownMs, getSessionAffinity, getSessionAffinityMaxEntries, getSessionAffinityTtlMs, getStreamStallTimeoutMs, getMinRotationIntervalMs, getTokenInvalidationCooldownMs, getTokenRefreshSkewMs, getPidOffsetEnabled, getPreemptiveQuotaEnabled, getPreemptiveQuotaMaxDeferralMs, getPreemptiveQuotaRemainingPercent5h, getPreemptiveQuotaRemainingPercent7d, getRoutingMutexMode, getSchedulingStrategy, loadPluginConfig, } from "./config.js";
6
6
  import { CODEX_BASE_URL, HTTP_STATUS, OPENAI_HEADERS, OPENAI_HEADER_VALUES, URL_PATHS, } from "./constants.js";
@@ -15,6 +15,7 @@ import { CodexValidationError } from "./errors.js";
15
15
  import { normalizeEmailKey } from "./storage/identity.js";
16
16
  import { buildPinnedUnavailableErrorBody, buildTokenInvalidationBody, extractErrorCodeFromBody, isTokenInvalidationError, normalizeExhaustionStatus, parseRetryAfterBodyMs, parseRetryAfterHeaderMs, } from "./request/rate-limit-decision.js";
17
17
  import { forwardStreamingResponse, HOP_BY_HOP_HEADERS, readErrorBody, responseHeadersForClient, withTimeout, } from "./request/stream-failover-runtime.js";
18
+ import { getAccountRecoveryTimeForFamily } from "./runtime/account-status.js";
18
19
  import { chooseAccount } from "./runtime/rotation-account-selection.js";
19
20
  import { createRotationProxyState, recoverStaleRuntimeState, } from "./runtime/rotation-proxy-state.js";
20
21
  import { readStorageMetaFromDisk } from "./runtime/rotation-storage-meta.js";
@@ -62,6 +63,18 @@ function toUrlHost(host) {
62
63
  // failures surfaced only as a last-write-wins status.lastError string. Logs are
63
64
  // level-gated and carry the per-request correlation id set in handleRequest.
64
65
  const proxyLog = createLogger("runtime-proxy");
66
+ /**
67
+ * Pinned skip reasons that no timer clears: the account stays unselectable
68
+ * after any concurrent rate-limit record or cooldown expires, so the pinned
69
+ * 503 must not advertise that record's expiry as a recovery time.
70
+ */
71
+ const PINNED_PERMANENT_SKIP_REASONS = new Set([
72
+ "missing",
73
+ "disabled",
74
+ "workspace-disabled",
75
+ "policy-blocked",
76
+ AUTH_INVALIDATION_MARKER,
77
+ ]);
65
78
  const DEFAULT_QUOTA_REMAINING_THRESHOLD = 10;
66
79
  /** @internal Stable identity key for in-memory quota snapshots across reloads. */
67
80
  export function buildQuotaScheduleKey(account, family, model) {
@@ -600,6 +613,10 @@ export async function startRuntimeRotationProxy(options) {
600
613
  await closeServer(server, sockets);
601
614
  await state.activeAccountManager.flushPendingSave();
602
615
  },
616
+ // Live client connections, which the app helper reads as evidence that a
617
+ // detached consumer is still attached: a helper whose launcher is gone
618
+ // and whose socket set is empty has nobody left to serve.
619
+ getOpenConnectionCount: () => sockets.size,
603
620
  getStatus: () => ({
604
621
  ...state.status,
605
622
  // Redact any email/token material that leaked into a raw upstream or
@@ -1166,7 +1183,60 @@ async function handleRequestInner(state, req, res, traceId) {
1166
1183
  // null reason indicates a forecast/runtime state desync (the pinned
1167
1184
  // account was selected but no skip reason was recorded) — see #486.
1168
1185
  if (isPinned) {
1169
- const errorBody = buildPinnedUnavailableErrorBody(pinnedIndex, accountSkipReasons);
1186
+ const pinnedAccount = typeof pinnedIndex === "number"
1187
+ ? accountManager.getAccountByIndex(pinnedIndex)
1188
+ : null;
1189
+ const pinnedSkipReason = typeof pinnedIndex === "number"
1190
+ ? accountSkipReasons.get(pinnedIndex) ?? null
1191
+ : null;
1192
+ // A permanent blocker (disabled, no enabled workspace, invalidated
1193
+ // auth, policy block, out-of-range pin) outlives every timed record,
1194
+ // so advertising a record's expiry would invite a retry into another
1195
+ // 503.
1196
+ //
1197
+ // The recorded reason alone is not enough to detect one. It is the
1198
+ // SELECTION verdict, and this request can make the pin permanently
1199
+ // unselectable after selection already ran: a workspace-disabled
1200
+ // 402/403 calls setAccountEnabled(index, false) above and then
1201
+ // continues, so the next pass records "already-attempted" and the
1202
+ // disable never surfaces. With a breaker tripped by the same failure
1203
+ // the 503 would then advertise the circuit's ~30s reset for an
1204
+ // account no timer will ever re-admit. Re-read the pin's CURRENT
1205
+ // runtime state so a permanent blocker cannot hide behind the
1206
+ // verdict; the recorded reason still covers the selection-only
1207
+ // verdicts ("missing", "policy-blocked") that state cannot express.
1208
+ const pinnedCurrentSkipReason = pinnedAccount === null
1209
+ ? null
1210
+ : accountManager.getManagedAccountRuntimeSkipReason(pinnedAccount, context.family, context.model);
1211
+ const pinnedBlockedPermanently = (pinnedSkipReason !== null &&
1212
+ PINNED_PERMANENT_SKIP_REASONS.has(pinnedSkipReason)) ||
1213
+ (pinnedCurrentSkipReason !== null &&
1214
+ PINNED_PERMANENT_SKIP_REASONS.has(pinnedCurrentSkipReason));
1215
+ // Otherwise recovery comes from the pinned account's persisted state, not the
1216
+ // recorded skip reason: a direct 429/cooldown on the pinned account
1217
+ // reaches this 503 as "already-attempted" (the retry loop's selection
1218
+ // verdict) while the record it just wrote is what actually bounds
1219
+ // recovery — and with several overlapping records the account stays
1220
+ // skipped until the LAST one expires, so the latest bound is the one
1221
+ // worth advertising.
1222
+ const pinnedStateRecoveryAtMs = pinnedAccount === null
1223
+ ? null
1224
+ : getAccountRecoveryTimeForFamily(pinnedAccount, state.now(), context.family, context.model);
1225
+ // An open circuit outlives the short failure cooldowns that tripped
1226
+ // it; its deadline lives in the breaker, not the account record.
1227
+ const pinnedCircuitRecoveryAtMs = pinnedAccount === null
1228
+ ? null
1229
+ : accountManager.getCircuitRecoveryTime(pinnedAccount, state.now());
1230
+ const pinnedResetAtMs = pinnedBlockedPermanently ||
1231
+ (pinnedStateRecoveryAtMs === null && pinnedCircuitRecoveryAtMs === null)
1232
+ ? null
1233
+ : Math.max(pinnedStateRecoveryAtMs ?? 0, pinnedCircuitRecoveryAtMs ?? 0);
1234
+ const errorBody = buildPinnedUnavailableErrorBody(pinnedIndex, accountSkipReasons, {
1235
+ // typeof check so a forced index of 0 still reads as forced.
1236
+ pinSource: typeof state.forcedAccountIndex === "number" ? "forced" : "manual",
1237
+ resetAtMs: pinnedResetAtMs,
1238
+ now: state.now(),
1239
+ });
1170
1240
  if (errorBody.reason === null) {
1171
1241
  state.status.lastError = `pinned-503 missing skip reason (pinnedIndex=${pinnedIndex})`;
1172
1242
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.8.4",
3
+ "version": "2.8.6",
4
4
  "description": "Codex CLI multi-account OAuth manager with account switching, health checks, runtime rotation, diagnostics, and recovery tools for @openai/codex",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -174,7 +174,6 @@
174
174
  "dependencies": {
175
175
  "@codex-ai/plugin": "file:vendor/codex-ai-plugin",
176
176
  "@codex-ai/sdk": "file:vendor/codex-ai-sdk",
177
- "@openauthjs/openauth": "^0.4.3",
178
177
  "hono": "4.12.33",
179
178
  "undici": "6.28.0",
180
179
  "zod": "4.4.3"