@victor-software-house/pi-multicodex 2.3.0 → 2.3.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/account-manager.ts +51 -8
- package/hooks.ts +26 -21
- package/index.ts +1 -0
- package/package.json +1 -1
- package/selection.ts +20 -16
- package/stream-wrapper.ts +1 -0
- package/usage.ts +11 -0
package/account-manager.ts
CHANGED
|
@@ -30,11 +30,33 @@ export class AccountManager {
|
|
|
30
30
|
private manualEmail?: string;
|
|
31
31
|
private stateChangeHandlers = new Set<StateChangeHandler>();
|
|
32
32
|
private warnedAuthFailureEmails = new Set<string>();
|
|
33
|
+
private readyPromise: Promise<void> = Promise.resolve();
|
|
34
|
+
private readyResolve?: () => void;
|
|
33
35
|
|
|
34
36
|
constructor() {
|
|
35
37
|
this.data = loadStorage();
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Mark the account manager as initializing. The returned promise
|
|
42
|
+
* resolves when {@link markReady} is called. Stream requests wait
|
|
43
|
+
* on {@link waitUntilReady} so they don't race the startup refresh.
|
|
44
|
+
*/
|
|
45
|
+
beginInitialization(): void {
|
|
46
|
+
this.readyPromise = new Promise<void>((resolve) => {
|
|
47
|
+
this.readyResolve = resolve;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
markReady(): void {
|
|
52
|
+
this.readyResolve?.();
|
|
53
|
+
this.readyResolve = undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
waitUntilReady(): Promise<void> {
|
|
57
|
+
return this.readyPromise;
|
|
58
|
+
}
|
|
59
|
+
|
|
38
60
|
private save(): void {
|
|
39
61
|
saveStorage(this.data);
|
|
40
62
|
}
|
|
@@ -255,21 +277,29 @@ export class AccountManager {
|
|
|
255
277
|
const account = this.getAccount(email);
|
|
256
278
|
if (account) {
|
|
257
279
|
account.quotaExhaustedUntil = until;
|
|
258
|
-
this.
|
|
280
|
+
if (!this.isPiAuthAccount(account)) {
|
|
281
|
+
this.save();
|
|
282
|
+
}
|
|
259
283
|
this.notifyStateChanged();
|
|
260
284
|
}
|
|
261
285
|
}
|
|
262
286
|
|
|
263
287
|
clearAllQuotaExhaustion(): number {
|
|
264
288
|
let cleared = 0;
|
|
265
|
-
|
|
289
|
+
let managedChanged = false;
|
|
290
|
+
for (const account of this.getAccounts()) {
|
|
266
291
|
if (account.quotaExhaustedUntil) {
|
|
267
292
|
account.quotaExhaustedUntil = undefined;
|
|
268
293
|
cleared += 1;
|
|
294
|
+
if (!this.isPiAuthAccount(account)) {
|
|
295
|
+
managedChanged = true;
|
|
296
|
+
}
|
|
269
297
|
}
|
|
270
298
|
}
|
|
271
|
-
if (
|
|
299
|
+
if (managedChanged) {
|
|
272
300
|
this.save();
|
|
301
|
+
}
|
|
302
|
+
if (cleared > 0) {
|
|
273
303
|
this.notifyStateChanged();
|
|
274
304
|
}
|
|
275
305
|
return cleared;
|
|
@@ -377,7 +407,14 @@ export class AccountManager {
|
|
|
377
407
|
now,
|
|
378
408
|
});
|
|
379
409
|
if (selected) {
|
|
380
|
-
this.
|
|
410
|
+
if (this.isPiAuthAccount(selected)) {
|
|
411
|
+
// Don't persist ephemeral pi auth email to disk — it would
|
|
412
|
+
// become a stale activeEmail after restart.
|
|
413
|
+
this.data.activeEmail = selected.email;
|
|
414
|
+
this.notifyStateChanged();
|
|
415
|
+
} else {
|
|
416
|
+
this.setActiveAccount(selected.email);
|
|
417
|
+
}
|
|
381
418
|
}
|
|
382
419
|
return selected;
|
|
383
420
|
}
|
|
@@ -398,15 +435,21 @@ export class AccountManager {
|
|
|
398
435
|
}
|
|
399
436
|
|
|
400
437
|
private clearExpiredExhaustion(now: number): void {
|
|
401
|
-
let
|
|
402
|
-
|
|
438
|
+
let managedChanged = false;
|
|
439
|
+
let anyChanged = false;
|
|
440
|
+
for (const account of this.getAccounts()) {
|
|
403
441
|
if (account.quotaExhaustedUntil && account.quotaExhaustedUntil <= now) {
|
|
404
442
|
account.quotaExhaustedUntil = undefined;
|
|
405
|
-
|
|
443
|
+
anyChanged = true;
|
|
444
|
+
if (!this.isPiAuthAccount(account)) {
|
|
445
|
+
managedChanged = true;
|
|
446
|
+
}
|
|
406
447
|
}
|
|
407
448
|
}
|
|
408
|
-
if (
|
|
449
|
+
if (managedChanged) {
|
|
409
450
|
this.save();
|
|
451
|
+
}
|
|
452
|
+
if (anyChanged) {
|
|
410
453
|
this.notifyStateChanged();
|
|
411
454
|
}
|
|
412
455
|
}
|
package/hooks.ts
CHANGED
|
@@ -6,28 +6,33 @@ async function refreshAndActivateBestAccount(
|
|
|
6
6
|
accountManager: AccountManager,
|
|
7
7
|
warningHandler?: WarningHandler,
|
|
8
8
|
): Promise<void> {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
accountManager.beginInitialization();
|
|
10
|
+
try {
|
|
11
|
+
await accountManager.loadPiAuth();
|
|
12
|
+
await accountManager.refreshUsageForAllAccounts({ force: true });
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
const needsReauth = accountManager.getAccountsNeedingReauth();
|
|
15
|
+
if (needsReauth.length > 0) {
|
|
16
|
+
const hints = needsReauth.map((a) => {
|
|
17
|
+
const cmd = accountManager.isPiAuthAccount(a)
|
|
18
|
+
? "/login openai-codex"
|
|
19
|
+
: `/multicodex use ${a.email}`;
|
|
20
|
+
return `${a.email} (${cmd})`;
|
|
21
|
+
});
|
|
22
|
+
warningHandler?.(
|
|
23
|
+
`Multicodex: ${needsReauth.length} account(s) need re-authentication: ${hints.join(", ")}`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const manual = accountManager.getAvailableManualAccount();
|
|
28
|
+
if (manual) return;
|
|
29
|
+
if (accountManager.hasManualAccount()) {
|
|
30
|
+
accountManager.clearManualAccount();
|
|
31
|
+
}
|
|
32
|
+
await accountManager.activateBestAccount();
|
|
33
|
+
} finally {
|
|
34
|
+
accountManager.markReady();
|
|
29
35
|
}
|
|
30
|
-
await accountManager.activateBestAccount();
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export function handleSessionStart(
|
|
@@ -35,12 +40,12 @@ export function handleSessionStart(
|
|
|
35
40
|
warningHandler?: WarningHandler,
|
|
36
41
|
): void {
|
|
37
42
|
if (accountManager.getAccounts().length === 0) return;
|
|
38
|
-
|
|
43
|
+
refreshAndActivateBestAccount(accountManager, warningHandler).catch(() => {});
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
export function handleNewSessionSwitch(
|
|
42
47
|
accountManager: AccountManager,
|
|
43
48
|
warningHandler?: WarningHandler,
|
|
44
49
|
): void {
|
|
45
|
-
|
|
50
|
+
refreshAndActivateBestAccount(accountManager, warningHandler).catch(() => {});
|
|
46
51
|
}
|
package/index.ts
CHANGED
package/package.json
CHANGED
package/selection.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Account } from "./storage";
|
|
2
2
|
import {
|
|
3
3
|
type CodexUsageSnapshot,
|
|
4
|
+
getMaxUsedPercent,
|
|
4
5
|
getWeeklyResetAt,
|
|
5
6
|
isUsageUntouched,
|
|
6
7
|
} from "./usage";
|
|
@@ -15,20 +16,26 @@ function pickRandomAccount(accounts: Account[]): Account | undefined {
|
|
|
15
16
|
return accounts[Math.floor(Math.random() * accounts.length)];
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
function
|
|
19
|
+
function pickLowestUsageAccount(
|
|
19
20
|
accounts: Account[],
|
|
20
21
|
usageByEmail: Map<string, CodexUsageSnapshot>,
|
|
21
22
|
): Account | undefined {
|
|
22
23
|
const candidates = accounts
|
|
23
|
-
.map((account) =>
|
|
24
|
-
account
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
)
|
|
31
|
-
.sort((a, b) =>
|
|
24
|
+
.map((account) => {
|
|
25
|
+
const usage = usageByEmail.get(account.email);
|
|
26
|
+
return {
|
|
27
|
+
account,
|
|
28
|
+
usedPercent: getMaxUsedPercent(usage) ?? 100,
|
|
29
|
+
resetAt: getWeeklyResetAt(usage) ?? Number.MAX_SAFE_INTEGER,
|
|
30
|
+
};
|
|
31
|
+
})
|
|
32
|
+
.sort((a, b) => {
|
|
33
|
+
// Primary: lowest usage first
|
|
34
|
+
const usageDiff = a.usedPercent - b.usedPercent;
|
|
35
|
+
if (usageDiff !== 0) return usageDiff;
|
|
36
|
+
// Tiebreaker: earliest weekly reset first
|
|
37
|
+
return a.resetAt - b.resetAt;
|
|
38
|
+
});
|
|
32
39
|
|
|
33
40
|
return candidates[0]?.account;
|
|
34
41
|
}
|
|
@@ -55,16 +62,13 @@ export function pickBestAccount(
|
|
|
55
62
|
|
|
56
63
|
if (untouched.length > 0) {
|
|
57
64
|
return (
|
|
58
|
-
|
|
65
|
+
pickLowestUsageAccount(untouched, usageByEmail) ??
|
|
59
66
|
pickRandomAccount(untouched)
|
|
60
67
|
);
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
usageByEmail,
|
|
66
|
-
);
|
|
67
|
-
if (earliestWeeklyReset) return earliestWeeklyReset;
|
|
70
|
+
const lowestUsage = pickLowestUsageAccount(withUsage, usageByEmail);
|
|
71
|
+
if (lowestUsage) return lowestUsage;
|
|
68
72
|
|
|
69
73
|
return pickRandomAccount(available);
|
|
70
74
|
}
|
package/stream-wrapper.ts
CHANGED
package/usage.ts
CHANGED
|
@@ -66,6 +66,17 @@ export function getNextResetAt(usage?: CodexUsageSnapshot): number | undefined {
|
|
|
66
66
|
return Math.min(...candidates);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export function getMaxUsedPercent(
|
|
70
|
+
usage?: CodexUsageSnapshot,
|
|
71
|
+
): number | undefined {
|
|
72
|
+
const candidates = [
|
|
73
|
+
usage?.primary?.usedPercent,
|
|
74
|
+
usage?.secondary?.usedPercent,
|
|
75
|
+
].filter((value): value is number => typeof value === "number");
|
|
76
|
+
if (candidates.length === 0) return undefined;
|
|
77
|
+
return Math.max(...candidates);
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
export function getWeeklyResetAt(
|
|
70
81
|
usage?: CodexUsageSnapshot,
|
|
71
82
|
): number | undefined {
|