@henryqw/pi-multi-codex 3.0.0 → 3.1.1

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.
package/README.md CHANGED
@@ -11,6 +11,8 @@ Add multiple ChatGPT Codex OAuth accounts and start Pi work on the eligible slot
11
11
  pi install npm:@henryqw/pi-multi-codex
12
12
  ```
13
13
 
14
+ Requires Pi 1.1.0 or later in the 1.x series.
15
+
14
16
  ## Works with
15
17
 
16
18
  | Package | Relationship | Purpose |
@@ -260,7 +260,6 @@ async function loadState(signal?: AbortSignal): Promise<UsageState> {
260
260
 
261
261
  async function saveState(state: UsageState, signal?: AbortSignal): Promise<void> {
262
262
  signal?.throwIfAborted();
263
- if (state.untrusted) return;
264
263
  const file = cachePath();
265
264
  const data = `${JSON.stringify({
266
265
  slots: [...state.slots.values()].sort((a, b) => a.slot - b.slot),
@@ -272,11 +271,11 @@ async function saveState(state: UsageState, signal?: AbortSignal): Promise<void>
272
271
  async function withCacheMutex<T>(operation: (signal: AbortSignal) => Promise<T>, signal?: AbortSignal): Promise<T> {
273
272
  signal?.throwIfAborted();
274
273
  await mkdir(extensionConfigDir("pi-multi-codex"), { recursive: true, mode: 0o700 });
275
- let release: (() => Promise<void>) | undefined;
276
- while (!release) {
274
+ const compromised = new AbortController();
275
+ let release: () => Promise<void>;
276
+ for (;;) {
277
277
  signal?.throwIfAborted();
278
278
  try {
279
- const compromised = new AbortController();
280
279
  release = await lock(cachePath(), {
281
280
  lockfilePath: cacheMutexPath(),
282
281
  realpath: false,
@@ -285,19 +284,19 @@ async function withCacheMutex<T>(operation: (signal: AbortSignal) => Promise<T>,
285
284
  retries: 0,
286
285
  onCompromised: (error) => compromised.abort(error),
287
286
  });
288
- const operationSignal = signal ? AbortSignal.any([signal, compromised.signal]) : compromised.signal;
289
- try {
290
- operationSignal.throwIfAborted();
291
- return await raceWithSignal(operation(operationSignal), operationSignal);
292
- } finally {
293
- await release().catch(() => undefined);
294
- }
287
+ break;
295
288
  } catch (error) {
296
289
  if (!(error && typeof error === "object" && "code" in error && String(error.code) === "ELOCKED")) throw error;
297
290
  await delay(CACHE_MUTEX_RETRY_MS, undefined, { signal, ref: false });
298
291
  }
299
292
  }
300
- throw new Error("Cache mutex acquisition ended unexpectedly.");
293
+ const operationSignal = signal ? AbortSignal.any([signal, compromised.signal]) : compromised.signal;
294
+ try {
295
+ operationSignal.throwIfAborted();
296
+ return await raceWithSignal(operation(operationSignal), operationSignal);
297
+ } finally {
298
+ await release().catch(() => undefined);
299
+ }
301
300
  }
302
301
 
303
302
  function isMeasured(snapshot: UsageSnapshot | undefined): snapshot is MeasuredSnapshot {
@@ -434,15 +433,10 @@ class CodexQuotaStatus {
434
433
 
435
434
  private change<T>(change: (state: UsageState) => { value: T; write: boolean }, signal?: AbortSignal): Promise<T | undefined> {
436
435
  const operation = this.writes.then(() => withCacheMutex(async (operationSignal) => {
437
- operationSignal.throwIfAborted();
438
436
  const state = await loadState(operationSignal);
439
- operationSignal.throwIfAborted();
440
437
  if (state.untrusted) return undefined;
441
438
  const result = change(state);
442
- if (result.write) {
443
- operationSignal.throwIfAborted();
444
- await saveState(state, operationSignal);
445
- }
439
+ if (result.write) await saveState(state, operationSignal);
446
440
  return result.value;
447
441
  }, signal));
448
442
  this.writes = operation.then(() => undefined, () => undefined);
@@ -468,7 +462,6 @@ class CodexQuotaStatus {
468
462
  const refresh = this.refreshContext;
469
463
  if (!refresh) return;
470
464
  void this.refreshDue(refresh)
471
- .catch(() => undefined)
472
465
  .then(() => {
473
466
  if (this.refreshContext === refresh) this.scheduleRefresh();
474
467
  });
@@ -507,20 +500,21 @@ class CodexQuotaStatus {
507
500
  this.timer = undefined;
508
501
  if (this.refreshContext === refresh) this.requestRefresh();
509
502
  }, delay);
510
- this.timer.unref?.();
503
+ this.timer.unref();
511
504
  }
512
505
 
513
506
  private async refreshDue(refresh: QuotaRefresh): Promise<void> {
514
507
  const credentials = readCodexCredentials();
515
508
  const state = await this.state();
516
- await Promise.all([...credentials.entries()].map(async ([slot, credential]) => {
517
- if (this.refreshContext !== refresh || this.active.has(slot)) return;
509
+ const now = Date.now();
510
+ for (const [slot, credential] of credentials) {
511
+ if (this.refreshContext !== refresh || this.active.has(slot)) continue;
518
512
  const identity = identityFor(credential);
519
- if (!identity) return;
513
+ if (!identity) continue;
520
514
  const snapshot = state.slots.get(slot);
521
- if (isFresh(snapshot, identity, Date.now()) || checkedRecently(snapshot, identity, Date.now())) return;
515
+ if (isFresh(snapshot, identity, now) || checkedRecently(snapshot, identity, now)) continue;
522
516
  this.launch(slot, identity, refresh);
523
- })).catch(() => undefined);
517
+ }
524
518
  }
525
519
 
526
520
  private launch(slot: number, identity: SlotIdentity, refresh: QuotaRefresh): void {
@@ -569,7 +563,7 @@ class CodexQuotaStatus {
569
563
  if (!kept) this.stopHeartbeat(slot, owner);
570
564
  }).catch(() => this.stopHeartbeat(slot, owner));
571
565
  }, HEARTBEAT_MS);
572
- timer.unref?.();
566
+ timer.unref();
573
567
  this.heartbeats.set(slot, { owner, timer });
574
568
  }
575
569
 
@@ -662,7 +656,7 @@ class CodexQuotaStatus {
662
656
  signal: fetchSignal,
663
657
  });
664
658
  if (!response.ok) throw new Error(`Codex usage request failed (${response.status}).`);
665
- const usage = parseCodexUsage(await raceWithSignal(response.json(), fetchSignal));
659
+ const usage = parseCodexUsage(await response.json());
666
660
  if (!usage) throw new Error("Codex usage response has no usable seven-day window.");
667
661
  if (!this.identityStillCurrent(slot, identity.accountHash) || !(await this.stillOwn(slot, owner, identity.accountHash))) {
668
662
  discard = true;
@@ -703,10 +697,6 @@ class CodexQuotaStatus {
703
697
  }
704
698
  }
705
699
 
706
- function nativeModel(model: CodexModel): CodexModel {
707
- return model.provider === NATIVE_PROVIDER_ID ? model : { ...model, provider: NATIVE_PROVIDER_ID };
708
- }
709
-
710
700
  function aliasModel(model: CodexModel, provider: string): CodexModel {
711
701
  return model.provider === provider ? model : { ...model, provider };
712
702
  }
@@ -717,10 +707,7 @@ function nativeContext(context: TranscriptContext, alias: string): TranscriptCon
717
707
  messages: context.messages.map((message) => {
718
708
  if (message.role !== "assistant") return message;
719
709
  const provider = message.provider === alias ? NATIVE_PROVIDER_ID : message.provider === NATIVE_PROVIDER_ID ? alias : message.provider;
720
- if (provider === message.provider) return message;
721
- const rewritten = { ...message, provider };
722
- if (message.deferred) rewritten.deferred = { ...message.deferred, provider };
723
- return rewritten;
710
+ return provider === message.provider ? message : aliasMessage(message, provider);
724
711
  }),
725
712
  };
726
713
  }
@@ -788,28 +775,28 @@ export function createCodexAliasProvider(native: CodexProvider, slot: number): C
788
775
  auth: native.auth,
789
776
  getModels: () => models,
790
777
  stream: (model, context, options?: ApiStreamOptions<"openai-codex-responses">) =>
791
- aliasStream(native.stream(nativeModel(model), nativeContext(context, providerId), options), model, providerId),
778
+ aliasStream(native.stream(aliasModel(model, NATIVE_PROVIDER_ID), nativeContext(context, providerId), options), model, providerId),
792
779
  streamSimple: (model, context, options?: SimpleStreamOptions) =>
793
- aliasStream(native.streamSimple(nativeModel(model), nativeContext(context, providerId), options), model, providerId),
780
+ aliasStream(native.streamSimple(aliasModel(model, NATIVE_PROVIDER_ID), nativeContext(context, providerId), options), model, providerId),
794
781
  };
795
782
 
796
783
  if (native.filterModels) {
797
784
  provider.filterModels = (available, credential) =>
798
785
  native
799
- .filterModels!(available.map(nativeModel), credential)
786
+ .filterModels!(available.map((model) => aliasModel(model, NATIVE_PROVIDER_ID)), credential)
800
787
  .map((model) => aliasModel(model, providerId));
801
788
  }
802
789
  if (native.fetchDeferred) {
803
790
  provider.fetchDeferred = (model, handle, options?: DeferredFetchOptions) =>
804
791
  aliasStream(
805
- native.fetchDeferred!(nativeModel(model), { ...handle, provider: NATIVE_PROVIDER_ID }, options),
792
+ native.fetchDeferred!(aliasModel(model, NATIVE_PROVIDER_ID), { ...handle, provider: NATIVE_PROVIDER_ID }, options),
806
793
  model,
807
794
  providerId,
808
795
  );
809
796
  }
810
797
  if (native.cancelDeferred) {
811
798
  provider.cancelDeferred = (model, handle, options?: DeferredCancelOptions) =>
812
- native.cancelDeferred!(nativeModel(model), { ...handle, provider: NATIVE_PROVIDER_ID }, options);
799
+ native.cancelDeferred!(aliasModel(model, NATIVE_PROVIDER_ID), { ...handle, provider: NATIVE_PROVIDER_ID }, options);
813
800
  }
814
801
  return provider;
815
802
  }
@@ -824,9 +811,9 @@ function isManagedModel(model: Model<any> | undefined): model is CodexModel {
824
811
  }
825
812
 
826
813
  function sessionHasAgentWork(ctx: ExtensionContext): boolean {
827
- return ctx.sessionManager?.getBranch?.().some((entry) =>
814
+ return ctx.sessionManager.getBranch().some((entry) =>
828
815
  entry.type === "message" || (entry.type === "custom" && entry.customType === AGENT_STARTED_ENTRY),
829
- ) ?? false;
816
+ );
830
817
  }
831
818
 
832
819
  export default function multiCodex(pi: ExtensionAPI): void {
@@ -863,7 +850,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
863
850
  };
864
851
 
865
852
  const allowsModel = (ctx: ExtensionContext, model: Model<any>, slot: number): boolean => {
866
- const scopedModels = ctx.scopedModels ?? [];
853
+ const scopedModels = ctx.scopedModels;
867
854
  return scopedModels.length === 0 || scopedModels.some(({ model: scoped }) => scoped.provider === providerForSlot(slot) && scoped.id === model.id);
868
855
  };
869
856
 
@@ -919,17 +906,17 @@ export default function multiCodex(pi: ExtensionAPI): void {
919
906
  const now = Date.now();
920
907
  if (isFiveHourLimited(snapshot, now)) {
921
908
  const text = `${prefix} · 5h limit · ${formatDuration(snapshot.limitedUntil! - now)}`;
922
- return ctx.ui.theme?.fg ? ctx.ui.theme.fg("error", text) : text;
909
+ return ctx.ui.theme.fg("error", text);
923
910
  }
924
911
  if (!isFresh(snapshot, identity, now)) return `${prefix} · stale`;
925
912
  const display = displayedReset(snapshot, now);
926
913
  const text = `${prefix} · ${formatPercent(snapshot.remaining)}% · ${display.label} ${formatDuration(display.time - now)}`;
927
914
  const color = snapshot.remaining >= 50 ? "success" : snapshot.remaining >= 25 ? "warning" : "error";
928
- return ctx.ui.theme?.fg ? ctx.ui.theme.fg(color, text) : text;
915
+ return ctx.ui.theme.fg(color, text);
929
916
  };
930
917
 
931
918
  const updateFooter = (ctx: ExtensionContext | undefined = sessionContext): void => {
932
- if (ctx) ctx.ui.setStatus?.("pi-multi-codex", footerText(ctx));
919
+ if (ctx) ctx.ui.setStatus("pi-multi-codex", footerText(ctx));
933
920
  };
934
921
 
935
922
  const setModelAutomatically = async (model: Model<any>): Promise<boolean> => {
@@ -1099,10 +1086,8 @@ export default function multiCodex(pi: ExtensionAPI): void {
1099
1086
  return;
1100
1087
  }
1101
1088
 
1102
- const authenticated = [...syncSlots()]
1103
- .filter((slot) => slot === 1 || registered.has(slot))
1104
- .sort((left, right) => left - right);
1105
- const scopedModels = ctx.scopedModels ?? [];
1089
+ const authenticated = [...syncSlots()].sort((left, right) => left - right);
1090
+ const scopedModels = ctx.scopedModels;
1106
1091
  const slots = authenticated.filter((slot) => allowsModel(ctx, model, slot));
1107
1092
  const currentSlot = slotForProvider(model.provider);
1108
1093
  if (slots.length === 0 || (scopedModels.length > 0 && authenticated.some((slot) => !allowsModel(ctx, model, slot)) && slots.every((slot) => slot === currentSlot))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-multi-codex",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "Use multiple ChatGPT Codex OAuth accounts in Pi.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -26,8 +26,8 @@
26
26
  "pack:check": "npm pack --dry-run"
27
27
  },
28
28
  "peerDependencies": {
29
- "@earendil-works/pi-ai": "^1.0.0",
30
- "@earendil-works/pi-coding-agent": "^1.0.0"
29
+ "@earendil-works/pi-ai": "^1.1.0",
30
+ "@earendil-works/pi-coding-agent": "^1.1.0"
31
31
  },
32
32
  "repository": {
33
33
  "type": "git",
@@ -51,8 +51,8 @@
51
51
  "proper-lockfile": "^4.1.2"
52
52
  },
53
53
  "devDependencies": {
54
- "@earendil-works/pi-ai": "1.0.0",
55
- "@earendil-works/pi-coding-agent": "1.0.0",
54
+ "@earendil-works/pi-ai": "1.1.0",
55
+ "@earendil-works/pi-coding-agent": "1.1.0",
56
56
  "@types/proper-lockfile": "^4.1.4"
57
57
  }
58
58
  }