dsh-subscriptions 0.3.0 → 0.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/README.md +2 -2
- package/lib/client/SubscriptionUsageBadge.d.ts +7 -1
- package/lib/client/SubscriptionUsageBadge.js +23 -4
- package/lib/client/locales.d.ts +2 -0
- package/lib/client/locales.js +2 -0
- package/lib/client.js +59 -29
- package/lib/client.js.map +1 -1
- package/lib/index.js +43 -30
- package/lib/providers/claude.d.ts +3 -1
- package/lib/providers/claude.js +2 -2
- package/lib/providers/pool-usage.d.ts +10 -9
- package/lib/providers/pool-usage.js +30 -19
- package/lib/providers/pool.d.ts +6 -6
- package/lib/providers/pool.js +24 -12
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -7819,22 +7819,29 @@ var PoolAdapter = class extends LlmAdapter {
|
|
|
7819
7819
|
throw this.exhaustedWithWarn(options.model, members, failures);
|
|
7820
7820
|
}
|
|
7821
7821
|
/**
|
|
7822
|
-
* Order the candidates for one request. Health
|
|
7823
|
-
*
|
|
7824
|
-
*
|
|
7825
|
-
*
|
|
7826
|
-
*
|
|
7827
|
-
* `switchMargin`.
|
|
7822
|
+
* Order the candidates for one request. Health and known exhaustion filter
|
|
7823
|
+
* both strategies: an applicable window at 100% must never become a paid
|
|
7824
|
+
* fallback. `quota_aware` then ranks by urgency (members without telemetry,
|
|
7825
|
+
* e.g. copilot, score zero and sink to the bottom of their class), while
|
|
7826
|
+
* near-full members stay as a last-resort tail in urgency order. The sticky
|
|
7827
|
+
* member keeps its lead unless a challenger out-scores it by `switchMargin`.
|
|
7828
7828
|
*/
|
|
7829
7829
|
async select(poolId, members, sessionId) {
|
|
7830
7830
|
const usable = members.filter((member) => this.options.adapters[member.provider] !== void 0 && this.options.health.isMemberAvailable(member.provider, member.account, member.model));
|
|
7831
7831
|
if (usable.length === 0) return [];
|
|
7832
|
-
const stickyMember = sessionId === void 0 ? void 0 : usable.find((member) => memberKey(member.provider, member.account, member.model) === this.sticky.get(stickyKey(poolId, sessionId)));
|
|
7833
|
-
if (this.options.strategy === "priority") return stickyMember === void 0 ? usable : [stickyMember, ...usable.filter((member) => member !== stickyMember)];
|
|
7834
7832
|
const quotas = new Map(await Promise.all(usable.map(async (member) => [member, await this.options.usage.quotaFor(member)])));
|
|
7835
|
-
const
|
|
7836
|
-
|
|
7833
|
+
const candidates = usable.filter((member) => quotas.get(member)?.exhausted !== true);
|
|
7834
|
+
if (candidates.length === 0) {
|
|
7835
|
+
const message = `${poolId}: all currently usable accounts have exhausted subscription quota; the request was stopped before spending paid credits. After an external reset, use Settings > Subscriptions > Refresh to re-check usage.`;
|
|
7836
|
+
this.warnOnce(message);
|
|
7837
|
+
throw new LlmError(message, QUOTA_EXCEEDED_CODE);
|
|
7838
|
+
}
|
|
7839
|
+
const stickyMember = sessionId === void 0 ? void 0 : candidates.find((member) => memberKey(member.provider, member.account, member.model) === this.sticky.get(stickyKey(poolId, sessionId)));
|
|
7840
|
+
if (this.options.strategy === "priority") return stickyMember === void 0 ? candidates : [stickyMember, ...candidates.filter((member) => member !== stickyMember)];
|
|
7841
|
+
const scored = candidates.filter((member) => quotas.get(member)?.available === true);
|
|
7842
|
+
const quotaFull = candidates.filter((member) => quotas.get(member)?.available === false);
|
|
7837
7843
|
scored.sort((a, b) => (quotas.get(b)?.urgency ?? 0) - (quotas.get(a)?.urgency ?? 0));
|
|
7844
|
+
quotaFull.sort((a, b) => (quotas.get(b)?.urgency ?? 0) - (quotas.get(a)?.urgency ?? 0));
|
|
7838
7845
|
if (stickyMember !== void 0 && scored.includes(stickyMember)) {
|
|
7839
7846
|
const best = scored[0];
|
|
7840
7847
|
const stickyUrgency = quotas.get(stickyMember)?.urgency ?? 0;
|
|
@@ -8254,21 +8261,19 @@ var PoolUsageTracker = class {
|
|
|
8254
8261
|
* (member selection must never block on the network mid-conversation). A
|
|
8255
8262
|
* failure still cooling down degrades immediately with no network call.
|
|
8256
8263
|
*
|
|
8257
|
-
*
|
|
8258
|
-
*
|
|
8259
|
-
*
|
|
8260
|
-
*
|
|
8261
|
-
* `snapshotFor`'s stale-display concern (the Settings page, the composer
|
|
8262
|
-
* badge) has no such downside — showing an old percentage beats showing
|
|
8263
|
-
* nothing.
|
|
8264
|
+
* A failed refresh does not reuse stale urgency for routing order, but it
|
|
8265
|
+
* does retain a stale snapshot's known exhaustion bit. That one-way safety
|
|
8266
|
+
* guard prevents a failed Settings refresh from re-enabling paid-credit
|
|
8267
|
+
* fallback; an account with no successful usage sample still fails open.
|
|
8264
8268
|
* @param member - the pool member to score (account resolved).
|
|
8265
|
-
* @returns availability
|
|
8269
|
+
* @returns availability, known exhaustion, and the urgency score.
|
|
8266
8270
|
*/
|
|
8267
8271
|
async quotaFor(member) {
|
|
8268
8272
|
const key = `${member.provider}/${member.account}`;
|
|
8269
8273
|
const fetcher = this.fetcherFor(member.provider, member.account);
|
|
8270
8274
|
if (fetcher === void 0) return {
|
|
8271
8275
|
available: true,
|
|
8276
|
+
exhausted: false,
|
|
8272
8277
|
urgency: 0,
|
|
8273
8278
|
fetchedAt: 0
|
|
8274
8279
|
};
|
|
@@ -8279,7 +8284,7 @@ var PoolUsageTracker = class {
|
|
|
8279
8284
|
if (!fresh) this.refresh(key, fetcher).catch(() => void 0);
|
|
8280
8285
|
return this.score(member, entry);
|
|
8281
8286
|
}
|
|
8282
|
-
if (fresh) return
|
|
8287
|
+
if (fresh) return this.degraded(member, entry.error, entry.lastSnapshot);
|
|
8283
8288
|
}
|
|
8284
8289
|
try {
|
|
8285
8290
|
const snapshot = await this.refresh(key, fetcher);
|
|
@@ -8288,7 +8293,8 @@ var PoolUsageTracker = class {
|
|
|
8288
8293
|
at: Date.now()
|
|
8289
8294
|
});
|
|
8290
8295
|
} catch (error) {
|
|
8291
|
-
|
|
8296
|
+
const failed = this.entries.get(key);
|
|
8297
|
+
return this.degraded(member, error, failed?.snapshot ?? failed?.lastSnapshot);
|
|
8292
8298
|
}
|
|
8293
8299
|
}
|
|
8294
8300
|
/**
|
|
@@ -8394,17 +8400,27 @@ var PoolUsageTracker = class {
|
|
|
8394
8400
|
}
|
|
8395
8401
|
return pending;
|
|
8396
8402
|
}
|
|
8403
|
+
/** Degrade ordering while retaining only a cached safety exhaustion bit. */
|
|
8404
|
+
degraded(member, error, snapshot) {
|
|
8405
|
+
return degradedQuota(error, (snapshot === void 0 ? void 0 : this.score(member, {
|
|
8406
|
+
snapshot,
|
|
8407
|
+
at: 0
|
|
8408
|
+
}))?.exhausted === true);
|
|
8409
|
+
}
|
|
8397
8410
|
/** Score one member against a snapshot's windows. */
|
|
8398
8411
|
score(member, entry) {
|
|
8399
8412
|
const windows = (entry.snapshot.windows ?? []).filter((window) => windowApplies(window, member.model));
|
|
8400
8413
|
let available = true;
|
|
8414
|
+
let exhausted = false;
|
|
8401
8415
|
let urgency = 0;
|
|
8402
8416
|
for (const window of windows) {
|
|
8403
8417
|
if (window.usedPercent >= QUOTA_FULL_PERCENT) available = false;
|
|
8418
|
+
if (window.usedPercent >= 100) exhausted = true;
|
|
8404
8419
|
urgency = Math.max(urgency, windowUrgency(window));
|
|
8405
8420
|
}
|
|
8406
8421
|
return {
|
|
8407
8422
|
available,
|
|
8423
|
+
exhausted,
|
|
8408
8424
|
urgency,
|
|
8409
8425
|
fetchedAt: entry.at
|
|
8410
8426
|
};
|
|
@@ -8412,17 +8428,14 @@ var PoolUsageTracker = class {
|
|
|
8412
8428
|
};
|
|
8413
8429
|
/**
|
|
8414
8430
|
* The routing view of a fetch failure. Logged out: the member cannot serve
|
|
8415
|
-
* at all. Any other failure (network, endpoint rate limit)
|
|
8416
|
-
*
|
|
8417
|
-
*
|
|
8431
|
+
* at all. Any other failure (network, endpoint rate limit) has a zero urgency;
|
|
8432
|
+
* without a successful sample it stays available, while known exhaustion
|
|
8433
|
+
* remains unavailable so stale telemetry cannot re-enable paid fallback.
|
|
8418
8434
|
*/
|
|
8419
|
-
function degradedQuota(error) {
|
|
8420
|
-
return
|
|
8421
|
-
available:
|
|
8422
|
-
|
|
8423
|
-
fetchedAt: 0
|
|
8424
|
-
} : {
|
|
8425
|
-
available: true,
|
|
8435
|
+
function degradedQuota(error, exhausted = false) {
|
|
8436
|
+
return {
|
|
8437
|
+
available: !isMissingOrInvalidCredential(error) && !exhausted,
|
|
8438
|
+
exhausted,
|
|
8426
8439
|
urgency: 0,
|
|
8427
8440
|
fetchedAt: 0
|
|
8428
8441
|
};
|
|
@@ -172,7 +172,9 @@ export declare const CLAUDE_REQUEST_IMAGE_BUDGET: number;
|
|
|
172
172
|
* five-minute mark can still read an entry a one-hour mark wrote.
|
|
173
173
|
* @returns the JSON body to POST.
|
|
174
174
|
*/
|
|
175
|
-
export declare function claudeRequestBody(options: GenerateOptions,
|
|
175
|
+
export declare function claudeRequestBody(options: Omit<GenerateOptions, 'purpose'> & {
|
|
176
|
+
purpose?: string;
|
|
177
|
+
}, messages: readonly TranslatableMessage[], maxTokens: number, thinking?: Record<string, unknown>, effort?: string, cacheTtl?: PromptCacheTtl): Record<string, unknown>;
|
|
176
178
|
/** Claude wire adapter: one instance serves the `claude` provider route. */
|
|
177
179
|
export declare class ClaudeAdapter extends LlmAdapter {
|
|
178
180
|
private readonly options;
|
package/lib/providers/claude.js
CHANGED
|
@@ -580,8 +580,8 @@ const CLAUDE_MODALITIES = ['text', 'image'];
|
|
|
580
580
|
* @returns the JSON body to POST.
|
|
581
581
|
*/
|
|
582
582
|
export function claudeRequestBody(options, messages, maxTokens, thinking, effort, cacheTtl) {
|
|
583
|
-
//
|
|
584
|
-
//
|
|
583
|
+
// Auxiliary callers own this extension even when a host's GenerateOptions
|
|
584
|
+
// omits or reserves `purpose`; ordinary turns retain the configured TTL.
|
|
585
585
|
const purpose = options.purpose;
|
|
586
586
|
const ttl = purpose === undefined ? cacheTtl : undefined;
|
|
587
587
|
const anthropicMessages = toAnthropicMessages(messages);
|
|
@@ -19,8 +19,10 @@ export declare const QUOTA_FULL_PERCENT = 95;
|
|
|
19
19
|
export declare const USAGE_TTL_MS: number;
|
|
20
20
|
/** The scheduling view of one member's quota. */
|
|
21
21
|
export interface MemberQuota {
|
|
22
|
-
/**
|
|
22
|
+
/** Near-full gate: false at 95% or when the login is gone; may still serve as a fallback. */
|
|
23
23
|
available: boolean;
|
|
24
|
+
/** True at 100%: never spend this member's subscription quota or paid fallback. */
|
|
25
|
+
exhausted: boolean;
|
|
24
26
|
/** Required burn rate (fraction of window per ms); 0 when unknown. */
|
|
25
27
|
urgency: number;
|
|
26
28
|
/** Epoch ms of the snapshot this was computed from; 0 when none. */
|
|
@@ -51,15 +53,12 @@ export declare class PoolUsageTracker {
|
|
|
51
53
|
* (member selection must never block on the network mid-conversation). A
|
|
52
54
|
* failure still cooling down degrades immediately with no network call.
|
|
53
55
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* `snapshotFor`'s stale-display concern (the Settings page, the composer
|
|
59
|
-
* badge) has no such downside — showing an old percentage beats showing
|
|
60
|
-
* nothing.
|
|
56
|
+
* A failed refresh does not reuse stale urgency for routing order, but it
|
|
57
|
+
* does retain a stale snapshot's known exhaustion bit. That one-way safety
|
|
58
|
+
* guard prevents a failed Settings refresh from re-enabling paid-credit
|
|
59
|
+
* fallback; an account with no successful usage sample still fails open.
|
|
61
60
|
* @param member - the pool member to score (account resolved).
|
|
62
|
-
* @returns availability
|
|
61
|
+
* @returns availability, known exhaustion, and the urgency score.
|
|
63
62
|
*/
|
|
64
63
|
quotaFor(member: ConcretePoolMember): Promise<MemberQuota>;
|
|
65
64
|
/**
|
|
@@ -93,6 +92,8 @@ export declare class PoolUsageTracker {
|
|
|
93
92
|
* waiting out a stale cooldown.
|
|
94
93
|
*/
|
|
95
94
|
private refresh;
|
|
95
|
+
/** Degrade ordering while retaining only a cached safety exhaustion bit. */
|
|
96
|
+
private degraded;
|
|
96
97
|
/** Score one member against a snapshot's windows. */
|
|
97
98
|
private score;
|
|
98
99
|
}
|
|
@@ -49,21 +49,18 @@ export class PoolUsageTracker {
|
|
|
49
49
|
* (member selection must never block on the network mid-conversation). A
|
|
50
50
|
* failure still cooling down degrades immediately with no network call.
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* `snapshotFor`'s stale-display concern (the Settings page, the composer
|
|
57
|
-
* badge) has no such downside — showing an old percentage beats showing
|
|
58
|
-
* nothing.
|
|
52
|
+
* A failed refresh does not reuse stale urgency for routing order, but it
|
|
53
|
+
* does retain a stale snapshot's known exhaustion bit. That one-way safety
|
|
54
|
+
* guard prevents a failed Settings refresh from re-enabling paid-credit
|
|
55
|
+
* fallback; an account with no successful usage sample still fails open.
|
|
59
56
|
* @param member - the pool member to score (account resolved).
|
|
60
|
-
* @returns availability
|
|
57
|
+
* @returns availability, known exhaustion, and the urgency score.
|
|
61
58
|
*/
|
|
62
59
|
async quotaFor(member) {
|
|
63
60
|
const key = `${member.provider}/${member.account}`;
|
|
64
61
|
const fetcher = this.fetcherFor(member.provider, member.account);
|
|
65
62
|
if (fetcher === undefined)
|
|
66
|
-
return { available: true, urgency: 0, fetchedAt: 0 };
|
|
63
|
+
return { available: true, exhausted: false, urgency: 0, fetchedAt: 0 };
|
|
67
64
|
const entry = this.entries.get(key);
|
|
68
65
|
if (entry !== undefined) {
|
|
69
66
|
const fresh = Date.now() - entry.at < (entry.cooldownMs ?? this.ttlMs);
|
|
@@ -73,7 +70,7 @@ export class PoolUsageTracker {
|
|
|
73
70
|
return this.score(member, entry);
|
|
74
71
|
}
|
|
75
72
|
if (fresh)
|
|
76
|
-
return
|
|
73
|
+
return this.degraded(member, entry.error, entry.lastSnapshot);
|
|
77
74
|
// The cooldown expired: fall through to a fresh, blocking attempt.
|
|
78
75
|
}
|
|
79
76
|
try {
|
|
@@ -81,7 +78,8 @@ export class PoolUsageTracker {
|
|
|
81
78
|
return this.score(member, { snapshot, at: Date.now() });
|
|
82
79
|
}
|
|
83
80
|
catch (error) {
|
|
84
|
-
|
|
81
|
+
const failed = this.entries.get(key);
|
|
82
|
+
return this.degraded(member, error, failed?.snapshot ?? failed?.lastSnapshot);
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
85
|
/**
|
|
@@ -214,29 +212,42 @@ export class PoolUsageTracker {
|
|
|
214
212
|
}
|
|
215
213
|
return pending;
|
|
216
214
|
}
|
|
215
|
+
/** Degrade ordering while retaining only a cached safety exhaustion bit. */
|
|
216
|
+
degraded(member, error, snapshot) {
|
|
217
|
+
const cached = snapshot === undefined
|
|
218
|
+
? undefined
|
|
219
|
+
: this.score(member, { snapshot, at: 0 });
|
|
220
|
+
return degradedQuota(error, cached?.exhausted === true);
|
|
221
|
+
}
|
|
217
222
|
/** Score one member against a snapshot's windows. */
|
|
218
223
|
score(member, entry) {
|
|
219
224
|
const windows = (entry.snapshot.windows ?? []).filter((window) => windowApplies(window, member.model));
|
|
220
225
|
let available = true;
|
|
226
|
+
let exhausted = false;
|
|
221
227
|
let urgency = 0;
|
|
222
228
|
for (const window of windows) {
|
|
223
229
|
if (window.usedPercent >= QUOTA_FULL_PERCENT)
|
|
224
230
|
available = false;
|
|
231
|
+
if (window.usedPercent >= 100)
|
|
232
|
+
exhausted = true;
|
|
225
233
|
urgency = Math.max(urgency, windowUrgency(window));
|
|
226
234
|
}
|
|
227
|
-
return { available, urgency, fetchedAt: entry.at };
|
|
235
|
+
return { available, exhausted, urgency, fetchedAt: entry.at };
|
|
228
236
|
}
|
|
229
237
|
}
|
|
230
238
|
/**
|
|
231
239
|
* The routing view of a fetch failure. Logged out: the member cannot serve
|
|
232
|
-
* at all. Any other failure (network, endpoint rate limit)
|
|
233
|
-
*
|
|
234
|
-
*
|
|
240
|
+
* at all. Any other failure (network, endpoint rate limit) has a zero urgency;
|
|
241
|
+
* without a successful sample it stays available, while known exhaustion
|
|
242
|
+
* remains unavailable so stale telemetry cannot re-enable paid fallback.
|
|
235
243
|
*/
|
|
236
|
-
function degradedQuota(error) {
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
|
|
244
|
+
function degradedQuota(error, exhausted = false) {
|
|
245
|
+
return {
|
|
246
|
+
available: !isMissingOrInvalidCredential(error) && !exhausted,
|
|
247
|
+
exhausted,
|
|
248
|
+
urgency: 0,
|
|
249
|
+
fetchedAt: 0,
|
|
250
|
+
};
|
|
240
251
|
}
|
|
241
252
|
/** How long to hold a failure in the negative cache: the endpoint's own `retry-after`, or the default TTL. */
|
|
242
253
|
function cooldownFor(error, defaultTtlMs) {
|
package/lib/providers/pool.d.ts
CHANGED
|
@@ -95,12 +95,12 @@ export declare class PoolAdapter extends LlmAdapter {
|
|
|
95
95
|
*/
|
|
96
96
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
97
97
|
/**
|
|
98
|
-
* Order the candidates for one request. Health
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* `switchMargin`.
|
|
98
|
+
* Order the candidates for one request. Health and known exhaustion filter
|
|
99
|
+
* both strategies: an applicable window at 100% must never become a paid
|
|
100
|
+
* fallback. `quota_aware` then ranks by urgency (members without telemetry,
|
|
101
|
+
* e.g. copilot, score zero and sink to the bottom of their class), while
|
|
102
|
+
* near-full members stay as a last-resort tail in urgency order. The sticky
|
|
103
|
+
* member keeps its lead unless a challenger out-scores it by `switchMargin`.
|
|
104
104
|
*/
|
|
105
105
|
private select;
|
|
106
106
|
/** Pin the serving member to the session (with bounded memory). */
|
package/lib/providers/pool.js
CHANGED
|
@@ -287,31 +287,43 @@ export class PoolAdapter extends LlmAdapter {
|
|
|
287
287
|
throw this.exhaustedWithWarn(options.model, members, failures);
|
|
288
288
|
}
|
|
289
289
|
/**
|
|
290
|
-
* Order the candidates for one request. Health
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* `switchMargin`.
|
|
290
|
+
* Order the candidates for one request. Health and known exhaustion filter
|
|
291
|
+
* both strategies: an applicable window at 100% must never become a paid
|
|
292
|
+
* fallback. `quota_aware` then ranks by urgency (members without telemetry,
|
|
293
|
+
* e.g. copilot, score zero and sink to the bottom of their class), while
|
|
294
|
+
* near-full members stay as a last-resort tail in urgency order. The sticky
|
|
295
|
+
* member keeps its lead unless a challenger out-scores it by `switchMargin`.
|
|
296
296
|
*/
|
|
297
297
|
async select(poolId, members, sessionId) {
|
|
298
298
|
const usable = members.filter((member) => this.options.adapters[member.provider] !== undefined &&
|
|
299
299
|
this.options.health.isMemberAvailable(member.provider, member.account, member.model));
|
|
300
300
|
if (usable.length === 0)
|
|
301
301
|
return [];
|
|
302
|
+
const quotas = new Map(await Promise.all(usable.map(async (member) => [member, await this.options.usage.quotaFor(member)])));
|
|
303
|
+
const candidates = usable.filter((member) => quotas.get(member)?.exhausted !== true);
|
|
304
|
+
if (candidates.length === 0) {
|
|
305
|
+
const message = `${poolId}: all currently usable accounts have exhausted subscription quota; ` +
|
|
306
|
+
'the request was stopped before spending paid credits. After an external reset, ' +
|
|
307
|
+
'use Settings > Subscriptions > Refresh to re-check usage.';
|
|
308
|
+
this.warnOnce(message);
|
|
309
|
+
throw new LlmError(message, QUOTA_EXCEEDED_CODE);
|
|
310
|
+
}
|
|
302
311
|
const stickyMember = sessionId === undefined
|
|
303
312
|
? undefined
|
|
304
|
-
:
|
|
313
|
+
: candidates.find((member) => memberKey(member.provider, member.account, member.model) ===
|
|
305
314
|
this.sticky.get(stickyKey(poolId, sessionId)));
|
|
306
315
|
if (this.options.strategy === 'priority') {
|
|
307
316
|
return stickyMember === undefined
|
|
308
|
-
?
|
|
309
|
-
: [
|
|
317
|
+
? candidates
|
|
318
|
+
: [
|
|
319
|
+
stickyMember,
|
|
320
|
+
...candidates.filter((member) => member !== stickyMember),
|
|
321
|
+
];
|
|
310
322
|
}
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
const quotaFull = usable.filter((member) => quotas.get(member)?.available === false);
|
|
323
|
+
const scored = candidates.filter((member) => quotas.get(member)?.available === true);
|
|
324
|
+
const quotaFull = candidates.filter((member) => quotas.get(member)?.available === false);
|
|
314
325
|
scored.sort((a, b) => (quotas.get(b)?.urgency ?? 0) - (quotas.get(a)?.urgency ?? 0));
|
|
326
|
+
quotaFull.sort((a, b) => (quotas.get(b)?.urgency ?? 0) - (quotas.get(a)?.urgency ?? 0));
|
|
315
327
|
if (stickyMember !== undefined && scored.includes(stickyMember)) {
|
|
316
328
|
const best = scored[0];
|
|
317
329
|
const stickyUrgency = quotas.get(stickyMember)?.urgency ?? 0;
|