@henryqw/pi-multi-codex 2.0.2 → 2.0.3

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
@@ -61,6 +61,6 @@ The extension maintains a generated, credential-free quota cache at `~/.pi/agent
61
61
 
62
62
  ## Limits and recovery
63
63
 
64
- The extension reads `auth.json`. It never writes or refreshes credentials.
64
+ The extension reads `auth.json` to identify account slots but does not directly edit credentials. When it asks Pi's auth resolver for a token to check quota, Pi may refresh expired OAuth credentials and update its auth store.
65
65
 
66
66
  Scoped sessions can switch only to exact scoped aliases.
@@ -46,17 +46,21 @@ type SlotIdentity = {
46
46
  type SlotAuthResolver = (slot: number) => Promise<string | undefined>;
47
47
  type QuotaRefresh = { session: AbortController; resolveSlotAuth: SlotAuthResolver };
48
48
 
49
- type UsageSnapshot = {
50
- slot: number;
51
- accountHash: string;
52
- tier?: string;
53
- checkedAt: number;
54
- fetchedAt?: number;
55
- remaining?: number;
56
- reset?: number;
49
+ type SnapshotBase = { slot: number; accountHash: string; tier?: string; checkedAt: number };
50
+ type MeasuredSnapshot = SnapshotBase & {
51
+ fetchedAt: number;
52
+ remaining: number;
53
+ reset: number;
57
54
  fiveHourReset?: number;
58
- limitedUntil?: number | null;
55
+ limitedUntil: number | null;
59
56
  };
57
+ type UsageSnapshot = MeasuredSnapshot | (SnapshotBase & {
58
+ fetchedAt?: never;
59
+ remaining?: never;
60
+ reset?: never;
61
+ fiveHourReset?: never;
62
+ limitedUntil?: never;
63
+ });
60
64
 
61
65
  type UsageLock = {
62
66
  slot: number;
@@ -182,30 +186,27 @@ function validTier(value: unknown): value is string {
182
186
 
183
187
  function readSnapshot(value: unknown): UsageSnapshot | undefined {
184
188
  if (!isRecord(value) || !validSlot(value.slot) || !validHash(value.accountHash) || !validTime(value.checkedAt)) return undefined;
185
- const successful = value.fetchedAt !== undefined || value.remaining !== undefined || value.reset !== undefined;
186
- if (successful && (
187
- !validTime(value.fetchedAt) ||
188
- typeof value.remaining !== "number" ||
189
- !Number.isFinite(value.remaining) ||
190
- !validTime(value.reset) ||
191
- (value.fiveHourReset !== undefined && !validTime(value.fiveHourReset)) ||
192
- (value.limitedUntil !== null && !validTime(value.limitedUntil))
193
- )) return undefined;
194
- if (!successful && (value.fiveHourReset !== undefined || value.limitedUntil !== undefined)) return undefined;
195
- return {
189
+ const base = {
196
190
  slot: value.slot,
197
191
  accountHash: value.accountHash,
198
192
  ...(validTier(value.tier) ? { tier: value.tier } : {}),
199
193
  checkedAt: value.checkedAt,
200
- ...(successful
201
- ? {
202
- fetchedAt: value.fetchedAt as number,
203
- remaining: Math.max(0, Math.min(100, value.remaining as number)),
204
- reset: value.reset as number,
205
- ...(validTime(value.fiveHourReset) ? { fiveHourReset: value.fiveHourReset } : {}),
206
- limitedUntil: value.limitedUntil as number | null,
207
- }
208
- : {}),
194
+ };
195
+ const successful = value.fetchedAt !== undefined || value.remaining !== undefined || value.reset !== undefined;
196
+ if (!successful) {
197
+ if (value.fiveHourReset !== undefined || value.limitedUntil !== undefined) return undefined;
198
+ return base;
199
+ }
200
+ if (!validTime(value.fetchedAt) || typeof value.remaining !== "number" || !Number.isFinite(value.remaining) ||
201
+ !validTime(value.reset) || (value.fiveHourReset !== undefined && !validTime(value.fiveHourReset)) ||
202
+ (value.limitedUntil !== null && !validTime(value.limitedUntil))) return undefined;
203
+ return {
204
+ ...base,
205
+ fetchedAt: value.fetchedAt,
206
+ remaining: Math.max(0, Math.min(100, value.remaining)),
207
+ reset: value.reset,
208
+ ...(validTime(value.fiveHourReset) ? { fiveHourReset: value.fiveHourReset } : {}),
209
+ limitedUntil: value.limitedUntil,
209
210
  };
210
211
  }
211
212
 
@@ -299,12 +300,14 @@ async function withCacheMutex<T>(operation: (signal: AbortSignal) => Promise<T>,
299
300
  throw new Error("Cache mutex acquisition ended unexpectedly.");
300
301
  }
301
302
 
302
- function isFresh(snapshot: UsageSnapshot | undefined, identity: SlotIdentity, now: number): boolean {
303
+ function isMeasured(snapshot: UsageSnapshot | undefined): snapshot is MeasuredSnapshot {
304
+ return snapshot?.fetchedAt !== undefined;
305
+ }
306
+
307
+ function isFresh(snapshot: UsageSnapshot | undefined, identity: SlotIdentity, now: number): snapshot is MeasuredSnapshot {
303
308
  return Boolean(
304
- snapshot &&
309
+ isMeasured(snapshot) &&
305
310
  snapshot.accountHash === identity.accountHash &&
306
- validTime(snapshot.fetchedAt) &&
307
- validTime(snapshot.reset) &&
308
311
  snapshot.fetchedAt <= now &&
309
312
  snapshot.reset > now &&
310
313
  now - snapshot.fetchedAt < REFRESH_MS,
@@ -312,14 +315,14 @@ function isFresh(snapshot: UsageSnapshot | undefined, identity: SlotIdentity, no
312
315
  }
313
316
 
314
317
  function isFiveHourLimited(snapshot: UsageSnapshot, now: number): boolean {
315
- return validTime(snapshot.limitedUntil) && snapshot.limitedUntil > now;
318
+ return isMeasured(snapshot) && snapshot.limitedUntil !== null && snapshot.limitedUntil > now;
316
319
  }
317
320
 
318
- function displayedReset(snapshot: UsageSnapshot, now: number): { label: "5h" | "7d"; time: number } {
319
- if (["free", "go", "plus"].includes(snapshot.tier ?? "") && validTime(snapshot.fiveHourReset) && snapshot.fiveHourReset > now) {
321
+ function displayedReset(snapshot: MeasuredSnapshot, now: number): { label: "5h" | "7d"; time: number } {
322
+ if (["free", "go", "plus"].includes(snapshot.tier ?? "") && snapshot.fiveHourReset !== undefined && snapshot.fiveHourReset > now) {
320
323
  return { label: "5h", time: snapshot.fiveHourReset };
321
324
  }
322
- return { label: "7d", time: snapshot.reset! };
325
+ return { label: "7d", time: snapshot.reset };
323
326
  }
324
327
 
325
328
  function checkedRecently(snapshot: UsageSnapshot | undefined, identity: SlotIdentity, now: number): boolean {
@@ -328,7 +331,7 @@ function checkedRecently(snapshot: UsageSnapshot | undefined, identity: SlotIden
328
331
  snapshot.accountHash === identity.accountHash &&
329
332
  snapshot.checkedAt <= now &&
330
333
  now - snapshot.checkedAt < REFRESH_MS &&
331
- (!validTime(snapshot.fetchedAt) || (validTime(snapshot.reset) && snapshot.reset > now)),
334
+ (!isMeasured(snapshot) || snapshot.reset > now),
332
335
  );
333
336
  }
334
337
 
@@ -491,7 +494,7 @@ class CodexQuotaStatus {
491
494
  ? lock.heartbeatAt + LOCK_STALE_MS + 1
492
495
  : undefined;
493
496
  const snapshotDue = isFresh(snapshot, identity, now)
494
- ? Math.min(snapshot!.fetchedAt! + REFRESH_MS, snapshot!.reset!) + 1
497
+ ? Math.min(snapshot.fetchedAt + REFRESH_MS, snapshot.reset) + 1
495
498
  : checkedRecently(snapshot, identity, now)
496
499
  ? snapshot!.checkedAt + REFRESH_MS + 1
497
500
  : now;
@@ -614,7 +617,7 @@ class CodexQuotaStatus {
614
617
  limitedUntil: outcome.limitedUntil && outcome.limitedUntil > checkedAt ? outcome.limitedUntil : null,
615
618
  });
616
619
  } else {
617
- state.slots.set(slot, previous?.accountHash === identity.accountHash && (!validTime(previous.reset) || previous.reset > checkedAt)
620
+ state.slots.set(slot, previous?.accountHash === identity.accountHash && (!isMeasured(previous) || previous.reset > checkedAt)
618
621
  ? { ...previous, checkedAt }
619
622
  : { slot, accountHash: identity.accountHash, checkedAt });
620
623
  }
@@ -685,7 +688,7 @@ class CodexQuotaStatus {
685
688
  .map(([slot, credential]) => {
686
689
  const identity = identityFor(credential);
687
690
  const snapshot = state.slots.get(slot);
688
- if (!identity || !snapshot || snapshot.accountHash !== identity.accountHash || !validTime(snapshot.fetchedAt) || typeof snapshot.remaining !== "number" || !validTime(snapshot.reset)) {
691
+ if (!identity || !isMeasured(snapshot) || snapshot.accountHash !== identity.accountHash) {
689
692
  return `Codex slot ${slot}: unavailable`;
690
693
  }
691
694
  const tier = snapshot.tier ? ` (${snapshot.tier})` : "";
@@ -871,7 +874,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
871
874
  if ((slot !== 1 && !registered.has(slot)) || !allowsModel(ctx, model, slot)) return [];
872
875
  const identity = identityFor(credential);
873
876
  const snapshot = state.slots.get(slot);
874
- if (!identity || !isFresh(snapshot, identity, now) || typeof snapshot?.remaining !== "number" || isFiveHourLimited(snapshot, now)) return [];
877
+ if (!identity || !isFresh(snapshot, identity, now) || isFiveHourLimited(snapshot, now)) return [];
875
878
  return [{ slot, remaining: snapshot.remaining }];
876
879
  });
877
880
  };
@@ -895,7 +898,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
895
898
  const identity = identityFor(credential);
896
899
  const snapshot = state.slots.get(slot);
897
900
  if (identity && snapshot?.accountHash === identity.accountHash && isFiveHourLimited(snapshot, now)) return [];
898
- const remaining = identity && isFresh(snapshot, identity, now) ? snapshot?.remaining : undefined;
901
+ const remaining = identity && isFresh(snapshot, identity, now) ? snapshot.remaining : undefined;
899
902
  return [{ slot, remaining }];
900
903
  })
901
904
  .sort((left, right) => (right.remaining ?? -1) - (left.remaining ?? -1) || left.slot - right.slot)
@@ -909,7 +912,7 @@ export default function multiCodex(pi: ExtensionAPI): void {
909
912
  const identity = currentIdentity(slot);
910
913
  const snapshot = quota.snapshot(slot);
911
914
  const prefix = `Codex #${slot}`;
912
- if (!identity || !snapshot || snapshot.accountHash !== identity.accountHash || !validTime(snapshot.fetchedAt) || typeof snapshot.remaining !== "number" || !validTime(snapshot.reset)) {
915
+ if (!identity || !isMeasured(snapshot) || snapshot.accountHash !== identity.accountHash) {
913
916
  return `${prefix} · unavailable`;
914
917
  }
915
918
  const now = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-multi-codex",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Use multiple ChatGPT Codex OAuth accounts in Pi.",
5
5
  "keywords": [
6
6
  "pi-package",