@teamvibe/poller 0.8.7-rc.1 → 0.8.8
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/dist/claude-spawner.d.ts +2 -0
- package/dist/claude-spawner.js +42 -12
- package/dist/event-queue.js +11 -0
- package/dist/inbox-manager.d.ts +4 -0
- package/dist/inbox-manager.js +29 -0
- package/dist/inbox-watcher.js +16 -3
- package/dist/poller.js +33 -9
- package/dist/pool-defer.d.ts +24 -1
- package/dist/pool-defer.js +101 -5
- package/dist/subscription-runtime.d.ts +2 -0
- package/dist/subscription-runtime.js +8 -2
- package/dist/subscription-selection.d.ts +21 -2
- package/dist/subscription-selection.js +91 -14
- package/dist/subscription-usage-store.d.ts +6 -1
- package/dist/subscription-usage-store.js +246 -13
- package/dist/types.d.ts +2 -0
- package/dist/usage-limit-messages.d.ts +3 -0
- package/dist/usage-limit-messages.js +22 -0
- package/dist/usage-probe-job.d.ts +2 -0
- package/dist/usage-probe-job.js +36 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -10,6 +10,13 @@ export declare const PROBE_USAGE_TTL_MS = 60000;
|
|
|
10
10
|
export declare const POOL_RECHECK_MS: number;
|
|
11
11
|
/** Safety hold from the rejection observation when the provider gives no reset. */
|
|
12
12
|
export declare const REJECTED_BLOCK_MS: number;
|
|
13
|
+
export declare const VERIFICATION_MAX_BACKOFF_MS: number;
|
|
14
|
+
export interface VerificationEvidence {
|
|
15
|
+
observedAt: number;
|
|
16
|
+
source: 'passive' | 'probe';
|
|
17
|
+
status: 'allowed' | 'allowed_warning';
|
|
18
|
+
percent: number;
|
|
19
|
+
}
|
|
13
20
|
export interface AccountIdentity {
|
|
14
21
|
workspaceId: string;
|
|
15
22
|
accountId: string;
|
|
@@ -47,6 +54,11 @@ export interface UsageWindow {
|
|
|
47
54
|
source?: 'passive' | 'probe';
|
|
48
55
|
percent?: number;
|
|
49
56
|
resetAt?: number;
|
|
57
|
+
/** Missing on legacy rejections means the first consecutive rejection. */
|
|
58
|
+
rejectionCount?: number;
|
|
59
|
+
lastVerificationAttemptAt?: number;
|
|
60
|
+
/** A genuine same-window measurement; never inferred from an omitted meter. */
|
|
61
|
+
verificationEvidence?: VerificationEvidence;
|
|
50
62
|
}
|
|
51
63
|
export interface UsageObservation {
|
|
52
64
|
pollerVersion?: string;
|
|
@@ -67,11 +79,11 @@ export declare function projectLegacyFableUsage<T extends UsageObservation | und
|
|
|
67
79
|
* Deduplicate uncertain windows before splitting measurements from rejections.
|
|
68
80
|
*/
|
|
69
81
|
export declare function supersedeLegacyFableProjections(observations: readonly (UsageObservation | undefined)[], now: number): (UsageObservation | undefined)[];
|
|
70
|
-
/** Latest measurements plus durable
|
|
82
|
+
/** Latest measurements plus durable rejections; only a claimed real spawn can clear a hold. */
|
|
71
83
|
export declare function mergeUsageObservations(observations: readonly (UsageObservation | undefined)[], now: number): UsageObservation | undefined;
|
|
72
84
|
/** Preserve last-known meters, projecting legacy identity before its global companion is replaced. */
|
|
73
85
|
export declare function mergePersistedUsageObservations(previous: UsageObservation | undefined, incoming: UsageObservation | undefined, now: number): UsageObservation | undefined;
|
|
74
|
-
export interface ReactiveExhaustion {
|
|
86
|
+
export interface ReactiveExhaustion extends Pick<UsageWindow, 'rejectionCount' | 'lastVerificationAttemptAt' | 'verificationEvidence'> {
|
|
75
87
|
observedAt: number;
|
|
76
88
|
quota: ClaudeQuotaFailure;
|
|
77
89
|
}
|
|
@@ -106,6 +118,13 @@ export declare function record(value: unknown): Record<string, unknown>;
|
|
|
106
118
|
export declare function normalizePassiveUsage(value: unknown, observedAt: number): UsageObservation;
|
|
107
119
|
export declare function normalizeProbeUsage(value: unknown, observedAt: number): UsageObservation;
|
|
108
120
|
export declare function family(model: string | undefined): 'sonnet' | 'opus' | 'haiku' | 'fable' | undefined;
|
|
121
|
+
export declare function rejectionResetAt(window: UsageWindow, observedAt: number): number;
|
|
122
|
+
export declare function verificationEligibleAt(window: UsageWindow, observedAt: number): number;
|
|
123
|
+
export declare function rejectionEvidence(usage: UsageObservation | undefined, window: UsageWindow, now: number): VerificationEvidence | undefined;
|
|
124
|
+
export declare function rejectionEligibleAt(usage: UsageObservation | undefined, window: UsageWindow, account: AccountIdentity, now: number): number;
|
|
125
|
+
export declare function reactiveUsageWindow(reactive: ReactiveExhaustion): UsageWindow;
|
|
126
|
+
/** Due windows are claimed durably at the real spawn boundary, never by pure selection. */
|
|
127
|
+
export declare function verificationWindows(account: AccountIdentity, state: AccountUsageState, model: string | undefined, now: number): UsageWindow[];
|
|
109
128
|
export declare function reactiveEligibleAt(reactive: ReactiveExhaustion): number;
|
|
110
129
|
/** Pure policy. Unknown is eligible after every known/reactive blocker is ruled out. */
|
|
111
130
|
export declare function selectSubscriptionAccount<A extends AccountIdentity>(input: {
|
|
@@ -8,6 +8,7 @@ export const PROBE_USAGE_TTL_MS = 60_000;
|
|
|
8
8
|
export const POOL_RECHECK_MS = 15 * 60_000;
|
|
9
9
|
/** Safety hold from the rejection observation when the provider gives no reset. */
|
|
10
10
|
export const REJECTED_BLOCK_MS = 15 * 60_000;
|
|
11
|
+
export const VERIFICATION_MAX_BACKOFF_MS = 8 * 60 * 60_000;
|
|
11
12
|
/** Passive and probe percentages remain lower bounds until each window resets. */
|
|
12
13
|
export function usageWindowExpiresAt(usage, window) {
|
|
13
14
|
const ttlExpiry = (window.observedAt ?? usage.observedAt) + ((window.source ?? usage.source) === 'passive' ? PASSIVE_USAGE_TTL_MS : PROBE_USAGE_TTL_MS);
|
|
@@ -116,7 +117,7 @@ export function supersedeLegacyFableProjections(observations, now) {
|
|
|
116
117
|
}),
|
|
117
118
|
}));
|
|
118
119
|
}
|
|
119
|
-
/** Latest measurements plus durable
|
|
120
|
+
/** Latest measurements plus durable rejections; only a claimed real spawn can clear a hold. */
|
|
120
121
|
export function mergeUsageObservations(observations, now) {
|
|
121
122
|
let latest;
|
|
122
123
|
const rejected = new Map();
|
|
@@ -139,13 +140,24 @@ export function mergeUsageObservations(observations, now) {
|
|
|
139
140
|
if (w.status !== 'rejected')
|
|
140
141
|
continue;
|
|
141
142
|
const previous = rejected.get(key);
|
|
142
|
-
if (!previous || observedAt >= previous.observedAt)
|
|
143
|
-
|
|
143
|
+
if (!previous || observedAt >= previous.observedAt) {
|
|
144
|
+
const consecutive = previous && observedAt > previous.observedAt && observedAt < rejectionResetAt(previous, previous.observedAt);
|
|
145
|
+
rejected.set(key, {
|
|
146
|
+
...(previous?.observedAt === observedAt ? previous : {}),
|
|
147
|
+
...w, observedAt, source: w.source ?? usage.source,
|
|
148
|
+
...(consecutive && w.resetAt === undefined && previous.resetAt !== undefined ? { resetAt: previous.resetAt } : {}),
|
|
149
|
+
...(consecutive || w.rejectionCount !== undefined || previous?.observedAt === observedAt && previous.rejectionCount !== undefined
|
|
150
|
+
? { rejectionCount: Math.max(w.rejectionCount ?? 1, consecutive ? (previous.rejectionCount ?? 1) + 1 : previous?.rejectionCount ?? 1) } : {}),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
144
153
|
}
|
|
145
154
|
}
|
|
146
155
|
if (!latest)
|
|
147
156
|
return undefined;
|
|
148
|
-
const active = [...rejected.values()].filter(w => now < (w
|
|
157
|
+
const active = [...rejected.values()].filter(w => now < rejectionResetAt(w, w.observedAt)).map(w => {
|
|
158
|
+
const evidence = latestVerificationEvidence(observations, w, w.observedAt, now);
|
|
159
|
+
return evidence ? { ...w, verificationEvidence: evidence } : w;
|
|
160
|
+
});
|
|
149
161
|
const retained = [...scopedExhausted.values()].filter(w => isScopedExhaustion(w) &&
|
|
150
162
|
!latest.windows.some(other => other.window === w.window && other.scope === w.scope) &&
|
|
151
163
|
!active.some(other => other.window === w.window && other.scope === w.scope) &&
|
|
@@ -281,7 +293,9 @@ export function normalizeProbeUsage(value, observedAt) {
|
|
|
281
293
|
if (raw == null)
|
|
282
294
|
continue;
|
|
283
295
|
const item = record(raw);
|
|
284
|
-
windows.push({ window, scope, percent: percent(item['utilization'], 100), resetAt: date(item['resets_at'])
|
|
296
|
+
windows.push({ window, scope, percent: percent(item['utilization'], 100), resetAt: date(item['resets_at']),
|
|
297
|
+
...(percent(item['utilization'], 100) !== undefined ? { status: 'allowed' } : {}),
|
|
298
|
+
});
|
|
285
299
|
}
|
|
286
300
|
// The optional endpoint is undocumented. Accept only the explicit window /
|
|
287
301
|
// model form below; arbitrary internal codenames never map to a family.
|
|
@@ -295,7 +309,9 @@ export function normalizeProbeUsage(value, observedAt) {
|
|
|
295
309
|
}
|
|
296
310
|
const model = item['model'];
|
|
297
311
|
const scope = model === 'global' ? 'global' : typeof model === 'string' ? family(model) ?? 'uncertain' : 'uncertain';
|
|
298
|
-
windows.push({ window, scope, percent: percent(item['utilization'], 100), resetAt: date(item['resets_at'])
|
|
312
|
+
windows.push({ window, scope, percent: percent(item['utilization'], 100), resetAt: date(item['resets_at']),
|
|
313
|
+
...(percent(item['utilization'], 100) !== undefined ? { status: 'allowed' } : {}),
|
|
314
|
+
});
|
|
299
315
|
}
|
|
300
316
|
if (info['limits'].length > 128)
|
|
301
317
|
windows.push({ window: 'seven_day', scope: 'uncertain' });
|
|
@@ -308,6 +324,63 @@ export function family(model) {
|
|
|
308
324
|
function applicable(scope, model) {
|
|
309
325
|
return scope === 'global' || scope === 'uncertain' || family(model) === undefined || scope === family(model);
|
|
310
326
|
}
|
|
327
|
+
export function rejectionResetAt(window, observedAt) {
|
|
328
|
+
return window.resetAt ?? observedAt + REJECTED_BLOCK_MS;
|
|
329
|
+
}
|
|
330
|
+
export function verificationEligibleAt(window, observedAt) {
|
|
331
|
+
return Math.min(rejectionResetAt(window, observedAt), observedAt +
|
|
332
|
+
Math.min(VERIFICATION_MAX_BACKOFF_MS, REJECTED_BLOCK_MS * 2 ** Math.min(5, Math.max(0, (window.rejectionCount ?? 1) - 1))));
|
|
333
|
+
}
|
|
334
|
+
function latestVerificationEvidence(observations, rejected, rejectedAt, now) {
|
|
335
|
+
let evidence = rejected.verificationEvidence;
|
|
336
|
+
if (evidence && (evidence.observedAt <= rejectedAt || evidence.observedAt > now))
|
|
337
|
+
evidence = undefined;
|
|
338
|
+
// Uncertain/projected identity cannot prove that the provider's exact quota recovered.
|
|
339
|
+
if (rejected.scope === 'uncertain' || rejected.kind === 'legacy-uncertain-seven-day')
|
|
340
|
+
return undefined;
|
|
341
|
+
for (const usage of observations) {
|
|
342
|
+
if (!usage || usage.observedAt > now)
|
|
343
|
+
continue;
|
|
344
|
+
for (const w of usage.windows) {
|
|
345
|
+
const observedAt = w.observedAt ?? usage.observedAt;
|
|
346
|
+
if (w.window !== rejected.window || w.scope !== rejected.scope || w.kind === 'legacy-uncertain-seven-day' ||
|
|
347
|
+
observedAt <= rejectedAt || observedAt > now || w.percent === undefined ||
|
|
348
|
+
(w.status !== 'allowed' && w.status !== 'allowed_warning'))
|
|
349
|
+
continue;
|
|
350
|
+
if (!evidence || observedAt > evidence.observedAt || observedAt === evidence.observedAt && w.percent > evidence.percent)
|
|
351
|
+
evidence = { observedAt, source: w.source ?? usage.source, status: w.status, percent: w.percent };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return evidence;
|
|
355
|
+
}
|
|
356
|
+
export function rejectionEvidence(usage, window, now) {
|
|
357
|
+
return latestVerificationEvidence([usage], window, window.observedAt ?? usage?.observedAt ?? now, now);
|
|
358
|
+
}
|
|
359
|
+
export function rejectionEligibleAt(usage, window, account, now) {
|
|
360
|
+
const observedAt = window.observedAt ?? usage?.observedAt ?? now;
|
|
361
|
+
const resetAt = rejectionResetAt(window, observedAt);
|
|
362
|
+
const evidence = rejectionEvidence(usage, window, now);
|
|
363
|
+
return evidence && evidence.percent < (account.useFullQuota ? 100 : USAGE_THRESHOLD_PERCENT) &&
|
|
364
|
+
!(window.lastVerificationAttemptAt !== undefined && window.lastVerificationAttemptAt >= observedAt)
|
|
365
|
+
? verificationEligibleAt(window, observedAt) : resetAt;
|
|
366
|
+
}
|
|
367
|
+
export function reactiveUsageWindow(reactive) {
|
|
368
|
+
return {
|
|
369
|
+
window: reactive.quota.window ?? 'seven_day',
|
|
370
|
+
scope: reactive.quota.modelScope ? family(reactive.quota.modelScope) ?? 'uncertain' : 'global',
|
|
371
|
+
status: 'rejected', observedAt: reactive.observedAt, resetAt: reactiveEligibleAt(reactive),
|
|
372
|
+
rejectionCount: reactive.rejectionCount, lastVerificationAttemptAt: reactive.lastVerificationAttemptAt,
|
|
373
|
+
verificationEvidence: reactive.verificationEvidence,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/** Due windows are claimed durably at the real spawn boundary, never by pure selection. */
|
|
377
|
+
export function verificationWindows(account, state, model, now) {
|
|
378
|
+
return [...(state.usage?.windows ?? []), ...(state.reactive ?? []).filter(r => r.quota.window !== undefined).map(reactiveUsageWindow)].filter(w => {
|
|
379
|
+
const observedAt = w.observedAt ?? state.usage?.observedAt ?? now;
|
|
380
|
+
return w.status === 'rejected' && observedAt <= now && applicable(w.scope, model) &&
|
|
381
|
+
now < rejectionResetAt(w, observedAt) && now >= rejectionEligibleAt(state.usage, w, account, now);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
311
384
|
export function reactiveEligibleAt(reactive) {
|
|
312
385
|
const parsed = reactive.quota.resetAt ? Date.parse(reactive.quota.resetAt) : NaN;
|
|
313
386
|
return Number.isFinite(parsed) && parsed > reactive.observedAt ? parsed : reactive.observedAt + POOL_RECHECK_MS;
|
|
@@ -342,6 +415,7 @@ export function selectSubscriptionAccount(input) {
|
|
|
342
415
|
continue;
|
|
343
416
|
const state = input.states[account.accountId];
|
|
344
417
|
const blockers = [];
|
|
418
|
+
const usage = supersedeLegacyFableProjections([state?.usage], input.now)[0];
|
|
345
419
|
for (const reactive of state?.reactive ?? []) {
|
|
346
420
|
// Like durable pause createdAt, future observations expire on clock rollback.
|
|
347
421
|
if (input.now < reactive.observedAt)
|
|
@@ -349,12 +423,11 @@ export function selectSubscriptionAccount(input) {
|
|
|
349
423
|
const scope = reactive.quota.modelScope;
|
|
350
424
|
if (scope && family(scope) && family(input.model) && family(scope) !== family(input.model))
|
|
351
425
|
continue;
|
|
352
|
-
const until = reactiveEligibleAt(reactive);
|
|
426
|
+
const until = reactive.quota.window ? rejectionEligibleAt(usage, reactiveUsageWindow(reactive), account, input.now) : reactiveEligibleAt(reactive);
|
|
353
427
|
if (input.now < until)
|
|
354
428
|
blockers.push(until);
|
|
355
429
|
}
|
|
356
|
-
|
|
357
|
-
// A provider rejection outlives snapshot freshness. Never score it as headroom.
|
|
430
|
+
// A contradicted rejection permits one claimed verification after backoff.
|
|
358
431
|
if (usage && usage.observedAt <= input.now) {
|
|
359
432
|
for (const w of usage.windows) {
|
|
360
433
|
if (w.status !== 'rejected' || !applicable(w.scope, input.model))
|
|
@@ -362,7 +435,7 @@ export function selectSubscriptionAccount(input) {
|
|
|
362
435
|
const observedAt = w.observedAt ?? usage.observedAt;
|
|
363
436
|
if (observedAt > input.now)
|
|
364
437
|
continue;
|
|
365
|
-
const until = w
|
|
438
|
+
const until = rejectionEligibleAt(usage, w, account, input.now);
|
|
366
439
|
if (input.now < until)
|
|
367
440
|
blockers.push(until);
|
|
368
441
|
}
|
|
@@ -414,14 +487,18 @@ export function blockingUsageWindows(account, state, model, now) {
|
|
|
414
487
|
const usage = supersedeLegacyFableProjections([state?.usage], now)[0];
|
|
415
488
|
const windows = [];
|
|
416
489
|
for (const window of usage?.windows ?? []) {
|
|
417
|
-
const
|
|
490
|
+
const evidence = window.status === 'rejected' ? rejectionEvidence(usage, window, now) : undefined;
|
|
491
|
+
const result = selectSubscriptionAccount({ pool, states: { [account.accountId]: { usage: { ...usage, windows: [{ ...window, ...(evidence ? { verificationEvidence: evidence } : {}) }] } } }, model, now });
|
|
418
492
|
if (result.kind === 'deferred')
|
|
419
493
|
windows.push({ ...window, eligibleAt: result.eligibleAt });
|
|
420
494
|
}
|
|
421
495
|
for (const reactive of state?.reactive ?? []) {
|
|
422
|
-
const
|
|
423
|
-
if (
|
|
424
|
-
|
|
496
|
+
const window = reactiveUsageWindow(reactive);
|
|
497
|
+
if (applicable(window.scope, model) && reactive.observedAt <= now) {
|
|
498
|
+
const eligibleAt = reactive.quota.window ? rejectionEligibleAt(usage, window, account, now) : reactiveEligibleAt(reactive);
|
|
499
|
+
if (now < eligibleAt)
|
|
500
|
+
windows.push({ ...window, resetAt: reactive.quota.resetAt ? Date.parse(reactive.quota.resetAt) : undefined, eligibleAt });
|
|
501
|
+
}
|
|
425
502
|
}
|
|
426
503
|
return windows;
|
|
427
504
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ClaudeRateLimitInfo } from './claude-rate-limit.js';
|
|
2
2
|
import type { ClaudeQuotaFailure } from './claude-quota.js';
|
|
3
|
-
import { type AccountIdentity, type AccountUsageState, type ReactiveExhaustion, type UsageObservation } from './subscription-selection.js';
|
|
3
|
+
import { type AccountIdentity, type AccountUsageState, type ReactiveExhaustion, type UsageObservation, type UsageWindow } from './subscription-selection.js';
|
|
4
4
|
export type UsageKey = Pick<AccountIdentity, 'workspaceId' | 'accountId' | 'credentialVersion'>;
|
|
5
5
|
export type ProbeStatus = 'attempted' | 'ok' | 'unsupported' | 'unauthorized' | 'unavailable' | 'cancelled';
|
|
6
6
|
export interface ProbeObservation {
|
|
@@ -34,6 +34,9 @@ export interface SubscriptionUsageStore {
|
|
|
34
34
|
claimLocalProbe(key: UsageKey, observedAt: number, minIntervalMs: number): Promise<boolean>;
|
|
35
35
|
claimProbeRequest(requestId: string, expiresAt: number): Promise<boolean>;
|
|
36
36
|
finishProbe(key: UsageKey, observation: ProbeObservation): Promise<void>;
|
|
37
|
+
/** Atomic revalidation and one durable claim immediately before a real spawn. */
|
|
38
|
+
beginVerification(key: AccountIdentity, model: string | undefined, observedAt: number): Promise<UsageWindow[] | undefined>;
|
|
39
|
+
completeVerification(key: UsageKey, attemptAt: number, observedAt: number): Promise<void>;
|
|
37
40
|
}
|
|
38
41
|
export declare function usageStateForSelection(state: StoredAccountUsage, now: number): AccountUsageState;
|
|
39
42
|
export declare class SubscriptionUsageStoreError extends Error {
|
|
@@ -54,4 +57,6 @@ export declare class FileSubscriptionUsageStore implements SubscriptionUsageStor
|
|
|
54
57
|
/** Request claims live in the local data directory, alongside usage, never in a brain. */
|
|
55
58
|
claimProbeRequest(requestId: string, expiresAt: number): Promise<boolean>;
|
|
56
59
|
finishProbe(key: UsageKey, observation: ProbeObservation): Promise<void>;
|
|
60
|
+
beginVerification(key: AccountIdentity, model: string | undefined, observedAt: number): Promise<UsageWindow[] | undefined>;
|
|
61
|
+
completeVerification(key: UsageKey, attemptAt: number, observedAt: number): Promise<void>;
|
|
57
62
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { redactStartupText } from './redact-text.js';
|
|
2
|
+
import { logger } from './logger.js';
|
|
2
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
5
|
import { join, resolve } from 'node:path';
|
|
5
6
|
import { parseClaudeRateLimitInfo } from './claude-rate-limit.js';
|
|
6
|
-
import { normalizePassiveUsage, mergePersistedUsageObservations, mergeUsageObservations, record, reactiveEligibleAt, family } from './subscription-selection.js';
|
|
7
|
+
import { normalizePassiveUsage, mergePersistedUsageObservations, mergeUsageObservations, record, reactiveEligibleAt, reactiveUsageWindow, rejectionEvidence, rejectionResetAt, verificationEligibleAt, verificationWindows, selectSubscriptionAccount, family, USAGE_THRESHOLD_PERCENT } from './subscription-selection.js';
|
|
7
8
|
export function usageStateForSelection(state, now) {
|
|
8
9
|
return {
|
|
9
10
|
usage: mergeUsageObservations([
|
|
@@ -80,12 +81,31 @@ function safeUsage(value, source) {
|
|
|
80
81
|
if (observedAt !== undefined)
|
|
81
82
|
validTime(observedAt);
|
|
82
83
|
const source = w['source'] === 'passive' || w['source'] === 'probe' ? w['source'] : undefined;
|
|
83
|
-
return { window, scope, ...((w['kind'] === 'seven_day_overage_included' || w['kind'] === 'legacy-uncertain-seven-day') ? { kind: w['kind'] } : {}), percent, resetAt, ...(status ? { status } : {}), ...(observedAt ? { observedAt } : {}), ...(source ? { source } : {}) };
|
|
84
|
+
return { window, scope, ...((w['kind'] === 'seven_day_overage_included' || w['kind'] === 'legacy-uncertain-seven-day') ? { kind: w['kind'] } : {}), percent, resetAt, ...(status ? { status } : {}), ...(observedAt ? { observedAt } : {}), ...(source ? { source } : {}), ...safeVerificationFields(w) };
|
|
84
85
|
});
|
|
85
86
|
if (usage['windows'].length > 133)
|
|
86
87
|
windows.push({ window: 'seven_day', scope: 'uncertain' });
|
|
87
88
|
return { source, observedAt: usage['observedAt'], windows };
|
|
88
89
|
}
|
|
90
|
+
function safeVerificationFields(value) {
|
|
91
|
+
const result = {};
|
|
92
|
+
const count = value['rejectionCount'];
|
|
93
|
+
if (typeof count === 'number' && Number.isSafeInteger(count) && count >= 1)
|
|
94
|
+
result.rejectionCount = count;
|
|
95
|
+
const attemptAt = value['lastVerificationAttemptAt'];
|
|
96
|
+
if (typeof attemptAt === 'number') {
|
|
97
|
+
validTime(attemptAt);
|
|
98
|
+
result.lastVerificationAttemptAt = attemptAt;
|
|
99
|
+
}
|
|
100
|
+
const evidence = record(value['verificationEvidence']);
|
|
101
|
+
if (typeof evidence['observedAt'] === 'number' && (evidence['source'] === 'probe' || evidence['source'] === 'passive') &&
|
|
102
|
+
(evidence['status'] === 'allowed' || evidence['status'] === 'allowed_warning') &&
|
|
103
|
+
typeof evidence['percent'] === 'number' && Number.isFinite(evidence['percent']) && evidence['percent'] >= 0 && evidence['percent'] <= 100) {
|
|
104
|
+
validTime(evidence['observedAt']);
|
|
105
|
+
result.verificationEvidence = { observedAt: evidence['observedAt'], source: evidence['source'], status: evidence['status'], percent: evidence['percent'] };
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
89
109
|
function safeProbe(value) {
|
|
90
110
|
const obj = record(value);
|
|
91
111
|
const status = obj['status'];
|
|
@@ -126,7 +146,7 @@ function safeReactive(value) {
|
|
|
126
146
|
if (typeof obj['observedAt'] !== 'number' || !obj['quota'] || typeof obj['quota'] !== 'object')
|
|
127
147
|
throw new SubscriptionUsageStoreError();
|
|
128
148
|
validTime(obj['observedAt']);
|
|
129
|
-
return { observedAt: obj['observedAt'], quota: safeQuota(obj['quota']) };
|
|
149
|
+
return { observedAt: obj['observedAt'], quota: safeQuota(obj['quota']), ...safeVerificationFields(obj) };
|
|
130
150
|
});
|
|
131
151
|
}
|
|
132
152
|
/** One file per version + one reactive file per workspace/account. No credentials or raw provider bodies. */
|
|
@@ -139,6 +159,84 @@ export class FileSubscriptionUsageStore {
|
|
|
139
159
|
throw new SubscriptionUsageStoreError();
|
|
140
160
|
return join(this.#directory, `${createHash('sha256').update(JSON.stringify(parts)).digest('hex')}.json`);
|
|
141
161
|
}
|
|
162
|
+
#claimPath(key, window, fallbackAt) {
|
|
163
|
+
// The epoch identifies the backoff step, so processes with different wall
|
|
164
|
+
// clocks still compete for the same exclusive marker.
|
|
165
|
+
const epoch = verificationEligibleAt(window, window.observedAt ?? fallbackAt);
|
|
166
|
+
return join(this.#directory, 'claims', `${encodeURIComponent(key.accountId)}.${window.scope}.${window.window}.${epoch}.claim`);
|
|
167
|
+
}
|
|
168
|
+
async #claimMarkers(key) {
|
|
169
|
+
const directory = join(this.#directory, 'claims');
|
|
170
|
+
const claims = new Map();
|
|
171
|
+
try {
|
|
172
|
+
const names = await readdir(directory);
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
for (const name of names) {
|
|
175
|
+
if (!name.startsWith(`${encodeURIComponent(key.accountId)}.`) || !name.endsWith('.claim'))
|
|
176
|
+
continue;
|
|
177
|
+
const path = join(directory, name);
|
|
178
|
+
try {
|
|
179
|
+
const raw = await readFile(path, 'utf8');
|
|
180
|
+
let marker;
|
|
181
|
+
try {
|
|
182
|
+
marker = JSON.parse(raw);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Exclusive creation already owns the step, including the interval
|
|
186
|
+
// before metadata is written or a crash during that write.
|
|
187
|
+
if (now - (await stat(path)).mtimeMs > 24 * 3600_000) {
|
|
188
|
+
await rm(path, { force: true });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const epoch = Number(name.split('.').at(-2));
|
|
192
|
+
validTime(epoch);
|
|
193
|
+
claims.set(path, epoch);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
validTime(marker.attemptAt);
|
|
197
|
+
validTime(marker.resetAt);
|
|
198
|
+
if (now - marker.attemptAt > 24 * 3600_000 || marker.resetAt <= now) {
|
|
199
|
+
await rm(path, { force: true });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
claims.set(path, marker.attemptAt);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (error.code !== 'ENOENT')
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return claims;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (error.code === 'ENOENT')
|
|
213
|
+
return claims;
|
|
214
|
+
throw new SubscriptionUsageStoreError();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async #claimVerification(key, windows, observedAt) {
|
|
218
|
+
try {
|
|
219
|
+
await mkdir(join(this.#directory, 'claims'), { recursive: true, mode: 0o700 });
|
|
220
|
+
const markers = new Map(windows.map(w => [this.#claimPath(key, w, observedAt), w]));
|
|
221
|
+
for (const [path, window] of [...markers].sort(([a], [b]) => a.localeCompare(b))) {
|
|
222
|
+
const file = await open(path, 'wx', 0o600);
|
|
223
|
+
try {
|
|
224
|
+
await file.writeFile(JSON.stringify({ attemptAt: observedAt, resetAt: rejectionResetAt(window, window.observedAt ?? observedAt) }));
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
await file.close();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (error.code === 'EEXIST')
|
|
234
|
+
return false;
|
|
235
|
+
// A marker remains spent even if the JSON write fails. Readers consult
|
|
236
|
+
// markers as well, so restarting cannot launch the same verification.
|
|
237
|
+
throw new SubscriptionUsageStoreError();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
142
240
|
async #load(path, fallback) {
|
|
143
241
|
try {
|
|
144
242
|
const raw = await readFile(path, 'utf8');
|
|
@@ -169,6 +267,49 @@ export class FileSubscriptionUsageStore {
|
|
|
169
267
|
await rm(temporary, { force: true }).catch(() => { });
|
|
170
268
|
}
|
|
171
269
|
}
|
|
270
|
+
#log(message) { logger.info(`[Pool] ${message}`); }
|
|
271
|
+
#logEvidence(previous, window, usage, now, threshold) {
|
|
272
|
+
const evidence = rejectionEvidence(usage, window, now);
|
|
273
|
+
if (evidence && evidence.percent < threshold && evidence.observedAt !== previous?.verificationEvidence?.observedAt &&
|
|
274
|
+
!(window.lastVerificationAttemptAt !== undefined && window.lastVerificationAttemptAt >= (window.observedAt ?? now))) {
|
|
275
|
+
this.#log(`rejection marked verifiable window=${window.window}/${window.scope} count=${window.rejectionCount ?? 1} evidence=${evidence.source}:${evidence.status}:${evidence.percent}% observedAt=${new Date(evidence.observedAt).toISOString()} eligibleAt=${new Date(verificationEligibleAt(window, window.observedAt ?? now)).toISOString()}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async #mergeUsage(key, state, previous, incoming, now) {
|
|
279
|
+
const path = this.#path(key, false);
|
|
280
|
+
const account = await this.#load(path, { reactive: [] });
|
|
281
|
+
account.reactive = safeReactive(account.reactive);
|
|
282
|
+
if (incoming)
|
|
283
|
+
incoming = { ...incoming, windows: incoming.windows.map(w => {
|
|
284
|
+
if (w.status !== 'rejected')
|
|
285
|
+
return w;
|
|
286
|
+
const at = w.observedAt ?? incoming.observedAt;
|
|
287
|
+
const prior = account.reactive.map(reactiveUsageWindow).find(r => r.window === w.window && r.scope === w.scope && r.observedAt <= at && r.resetAt > at);
|
|
288
|
+
return prior ? { ...w, rejectionCount: Math.max(w.rejectionCount ?? 1, (prior.rejectionCount ?? 1) + Number(prior.observedAt < at)) } : w;
|
|
289
|
+
}) };
|
|
290
|
+
state.usage = mergePersistedUsageObservations(previous, incoming, now);
|
|
291
|
+
const threshold = key.useFullQuota ? 100 : USAGE_THRESHOLD_PERCENT;
|
|
292
|
+
if (state.usage)
|
|
293
|
+
state.usage = { ...state.usage, windows: state.usage.windows.map(w => w.status === 'rejected' ? { ...w, rejectionCount: w.rejectionCount ?? 1 } : w) };
|
|
294
|
+
for (const window of state.usage?.windows ?? []) {
|
|
295
|
+
if (window.status !== 'rejected')
|
|
296
|
+
continue;
|
|
297
|
+
const old = previous?.windows.find(w => w.status === 'rejected' && w.window === window.window && w.scope === window.scope);
|
|
298
|
+
this.#logEvidence(old, window, state.usage, now, threshold);
|
|
299
|
+
if (old?.lastVerificationAttemptAt !== undefined && window.observedAt > old.observedAt)
|
|
300
|
+
this.#log(`verification re-rejected window=${window.window}/${window.scope} count=${window.rejectionCount ?? 1} eligibleAt=${new Date(verificationEligibleAt(window, window.observedAt)).toISOString()}`);
|
|
301
|
+
}
|
|
302
|
+
const reactive = safeReactive(account.reactive).map(r => {
|
|
303
|
+
if (!r.quota.window)
|
|
304
|
+
return r;
|
|
305
|
+
const window = reactiveUsageWindow(r);
|
|
306
|
+
const verificationEvidence = rejectionEvidence(state.usage, window, now);
|
|
307
|
+
this.#logEvidence(window, { ...window, verificationEvidence }, state.usage, now, threshold);
|
|
308
|
+
return { ...r, verificationEvidence };
|
|
309
|
+
});
|
|
310
|
+
if (JSON.stringify(reactive) !== JSON.stringify(account.reactive))
|
|
311
|
+
await this.#save(path, { reactive });
|
|
312
|
+
}
|
|
172
313
|
async read(key) {
|
|
173
314
|
const [version, account] = await Promise.all([
|
|
174
315
|
this.#load(this.#path(key, true), {}),
|
|
@@ -177,19 +318,32 @@ export class FileSubscriptionUsageStore {
|
|
|
177
318
|
// Corrupt reactive data must never silently resurrect an exhausted account.
|
|
178
319
|
if (!Array.isArray(account.reactive) || account.reactive.some(r => !r || !Number.isFinite(r.observedAt) || !r.quota))
|
|
179
320
|
throw new SubscriptionUsageStoreError();
|
|
180
|
-
|
|
321
|
+
const state = { ...safeVersion(version), reactive: safeReactive(account.reactive) };
|
|
322
|
+
const claims = await this.#claimMarkers(key);
|
|
323
|
+
const claimedWindow = (w, fallbackAt) => {
|
|
324
|
+
const attemptAt = claims.get(this.#claimPath(key, w, fallbackAt));
|
|
325
|
+
return w.status === 'rejected' && attemptAt !== undefined ? { ...w, lastVerificationAttemptAt: Math.max(w.lastVerificationAttemptAt ?? 0, attemptAt) } : w;
|
|
326
|
+
};
|
|
327
|
+
if (state.usage)
|
|
328
|
+
state.usage = { ...state.usage, windows: state.usage.windows.map(w => claimedWindow(w, state.usage.observedAt)) };
|
|
329
|
+
state.reactive = state.reactive.map(r => ({ ...r, lastVerificationAttemptAt: claimedWindow(reactiveUsageWindow(r), r.observedAt).lastVerificationAttemptAt }));
|
|
330
|
+
return state;
|
|
181
331
|
}
|
|
182
332
|
async recordPassive(key, rateLimitInfo, observedAt, options) {
|
|
183
333
|
validTime(observedAt);
|
|
184
334
|
const path = this.#path(key, true);
|
|
185
|
-
await serial(path, async () => {
|
|
335
|
+
await serial(this.#path(key, false), async () => {
|
|
186
336
|
const state = safeVersion(await this.#load(path, {}));
|
|
187
337
|
if (state.passive && state.passive.observedAt > observedAt && !options?.expireFutureObservations)
|
|
188
338
|
return;
|
|
189
339
|
// Retain Fable observation clocks even when the latest report omits its meter.
|
|
190
340
|
const previous = state.usage ?? usageStateForSelection({ ...state, reactive: [] }, observedAt).usage;
|
|
191
341
|
state.passive = { rateLimitInfo: safeRateLimitInfo(rateLimitInfo), observedAt };
|
|
192
|
-
|
|
342
|
+
const incoming = normalizePassiveUsage(state.passive.rateLimitInfo, observedAt);
|
|
343
|
+
const status = state.passive.rateLimitInfo.status;
|
|
344
|
+
if (status === 'allowed' || status === 'allowed_warning')
|
|
345
|
+
incoming.windows = incoming.windows.map(w => ({ ...w, status: w.status ?? status }));
|
|
346
|
+
await this.#mergeUsage(key, state, previous, incoming, observedAt);
|
|
193
347
|
await this.#save(path, state);
|
|
194
348
|
});
|
|
195
349
|
}
|
|
@@ -199,24 +353,41 @@ export class FileSubscriptionUsageStore {
|
|
|
199
353
|
await serial(path, async () => {
|
|
200
354
|
const state = await this.#load(path, { reactive: [] });
|
|
201
355
|
state.reactive = safeReactive(state.reactive).filter(r => !options?.expireFutureObservations || r.observedAt <= observedAt);
|
|
202
|
-
const next = { observedAt, quota: safeQuota(quota) };
|
|
356
|
+
const next = { observedAt, quota: safeQuota(quota), rejectionCount: 1 };
|
|
203
357
|
const until = reactiveEligibleAt;
|
|
204
358
|
const sameScope = (r) => r.quota.modelScope === next.quota.modelScope && r.quota.window === next.quota.window;
|
|
205
359
|
const previous = state.reactive.find(sameScope);
|
|
360
|
+
const version = safeVersion(await this.#load(this.#path(key, true), {}));
|
|
361
|
+
const window = reactiveUsageWindow(next);
|
|
362
|
+
const passive = version.usage?.windows.find(w => w.status === 'rejected' && w.window === window.window && w.scope === window.scope &&
|
|
363
|
+
(w.observedAt ?? version.usage.observedAt) <= observedAt && rejectionResetAt(w, w.observedAt ?? version.usage.observedAt) > observedAt);
|
|
206
364
|
const reactive = state.reactive.filter(r => !sameScope(r) && until(r) > observedAt);
|
|
207
365
|
const hasExplicitReset = (r) => r.quota.resetAt !== undefined && Date.parse(r.quota.resetAt) > r.observedAt;
|
|
208
366
|
// A recheck is not a reset. New matching quota evidence can replace that
|
|
209
367
|
// provisional deadline with its explicit reset, without clearing a known
|
|
210
368
|
// active reset or accepting an older observation as revalidation.
|
|
211
369
|
const revalidated = previous && next.observedAt >= previous.observedAt && !hasExplicitReset(previous) && hasExplicitReset(next);
|
|
212
|
-
|
|
370
|
+
if (previous && next.observedAt < previous.observedAt)
|
|
371
|
+
reactive.push(previous);
|
|
372
|
+
else if (previous && next.observedAt === previous.observedAt && !revalidated)
|
|
373
|
+
reactive.push(previous);
|
|
374
|
+
else {
|
|
375
|
+
next.rejectionCount = Math.max(previous && until(previous) > observedAt ? (previous.rejectionCount ?? 1) + Number(previous.observedAt < observedAt) : 1, passive ? (passive.rejectionCount ?? 1) + Number((passive.observedAt ?? version.usage.observedAt) < observedAt) : 1);
|
|
376
|
+
if (previous && !next.quota.resetAt && until(previous) > until(next))
|
|
377
|
+
next.quota.resetAt = previous.quota.resetAt;
|
|
378
|
+
if (!next.quota.resetAt && passive?.resetAt)
|
|
379
|
+
next.quota.resetAt = new Date(passive.resetAt).toISOString();
|
|
380
|
+
reactive.push(next);
|
|
381
|
+
if (previous?.lastVerificationAttemptAt !== undefined || passive?.lastVerificationAttemptAt !== undefined)
|
|
382
|
+
this.#log(`verification re-rejected window=${next.quota.window ?? 'seven_day'}/${next.quota.modelScope ?? 'global'} count=${next.rejectionCount} eligibleAt=${new Date(verificationEligibleAt(reactiveUsageWindow(next), observedAt)).toISOString()}`);
|
|
383
|
+
}
|
|
213
384
|
await this.#save(path, { reactive });
|
|
214
385
|
});
|
|
215
386
|
}
|
|
216
387
|
async beginProbe(key, observedAt) {
|
|
217
388
|
validTime(observedAt);
|
|
218
389
|
const path = this.#path(key, true);
|
|
219
|
-
return serial(path, async () => {
|
|
390
|
+
return serial(this.#path(key, false), async () => {
|
|
220
391
|
const state = safeVersion(await this.#load(path, {}));
|
|
221
392
|
if (state.probe)
|
|
222
393
|
return false;
|
|
@@ -229,7 +400,7 @@ export class FileSubscriptionUsageStore {
|
|
|
229
400
|
async claimLocalProbe(key, observedAt, minIntervalMs) {
|
|
230
401
|
validTime(observedAt);
|
|
231
402
|
const path = this.#path(key, true);
|
|
232
|
-
return serial(path, async () => {
|
|
403
|
+
return serial(this.#path(key, false), async () => {
|
|
233
404
|
const state = safeVersion(await this.#load(path, {}));
|
|
234
405
|
if (state.localProbeClaimedAt !== undefined && observedAt >= state.localProbeClaimedAt &&
|
|
235
406
|
observedAt - state.localProbeClaimedAt < minIntervalMs)
|
|
@@ -259,15 +430,77 @@ export class FileSubscriptionUsageStore {
|
|
|
259
430
|
async finishProbe(key, observation) {
|
|
260
431
|
validTime(observation.observedAt);
|
|
261
432
|
const path = this.#path(key, true);
|
|
262
|
-
await serial(path, async () => {
|
|
433
|
+
await serial(this.#path(key, false), async () => {
|
|
263
434
|
const state = safeVersion(await this.#load(path, {}));
|
|
264
435
|
// Network completion order must not discard a newer stored snapshot.
|
|
265
436
|
const mergeAt = Math.max(observation.observedAt, state.usage?.observedAt ?? 0, state.passive?.observedAt ?? 0);
|
|
266
437
|
// Retain Fable observation clocks even when the latest report omits its meter.
|
|
267
438
|
const previous = state.usage ?? usageStateForSelection({ ...state, reactive: [] }, mergeAt).usage;
|
|
268
439
|
state.probe = safeProbe(observation);
|
|
269
|
-
state
|
|
440
|
+
await this.#mergeUsage(key, state, previous, state.probe.usage, mergeAt);
|
|
270
441
|
await this.#save(path, state);
|
|
271
442
|
});
|
|
272
443
|
}
|
|
444
|
+
async beginVerification(key, model, observedAt) {
|
|
445
|
+
validTime(observedAt);
|
|
446
|
+
return serial(this.#path(key, false), async () => {
|
|
447
|
+
const stored = await this.read(key);
|
|
448
|
+
const state = usageStateForSelection(stored, observedAt);
|
|
449
|
+
const selected = selectSubscriptionAccount({ pool: { kind: 'ready', accounts: [key] }, states: { [key.accountId]: state }, model, now: observedAt });
|
|
450
|
+
if (selected.kind !== 'selected')
|
|
451
|
+
return undefined;
|
|
452
|
+
const windows = verificationWindows(key, state, model, observedAt);
|
|
453
|
+
if (!windows.length)
|
|
454
|
+
return [];
|
|
455
|
+
if (!await this.#claimVerification(key, windows, observedAt))
|
|
456
|
+
return undefined;
|
|
457
|
+
const matches = (w) => windows.some(v => v.window === w.window && v.scope === w.scope && v.observedAt === (w.observedAt ?? stored.usage?.observedAt ?? state.usage?.observedAt));
|
|
458
|
+
const version = safeVersion(await this.#load(this.#path(key, true), {}));
|
|
459
|
+
version.usage ??= state.usage;
|
|
460
|
+
if (version.usage)
|
|
461
|
+
version.usage = { ...version.usage, windows: version.usage.windows.map(w => matches(w) ? { ...w, lastVerificationAttemptAt: observedAt } : w) };
|
|
462
|
+
const reactive = stored.reactive.map(r => matches(reactiveUsageWindow(r)) ? { ...r, lastVerificationAttemptAt: observedAt } : r);
|
|
463
|
+
await this.#save(this.#path(key, true), version);
|
|
464
|
+
await this.#save(this.#path(key, false), { reactive });
|
|
465
|
+
for (const w of windows)
|
|
466
|
+
this.#log(`verification attempt started window=${w.window}/${w.scope} count=${w.rejectionCount ?? 1} attemptAt=${new Date(observedAt).toISOString()}`);
|
|
467
|
+
return windows;
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
async completeVerification(key, attemptAt, observedAt) {
|
|
471
|
+
validTime(attemptAt);
|
|
472
|
+
validTime(observedAt);
|
|
473
|
+
await serial(this.#path(key, false), async () => {
|
|
474
|
+
const stored = await this.read(key);
|
|
475
|
+
const cleared = [];
|
|
476
|
+
const canClear = (w) => w.status === 'rejected' && w.lastVerificationAttemptAt === attemptAt &&
|
|
477
|
+
(w.observedAt ?? stored.usage?.observedAt ?? observedAt) <= attemptAt && attemptAt <= observedAt;
|
|
478
|
+
const version = safeVersion(await this.#load(this.#path(key, true), {}));
|
|
479
|
+
const windows = version.usage?.windows.filter(w => { if (!canClear(w))
|
|
480
|
+
return true; cleared.push({ ...w, observedAt: w.observedAt ?? version.usage.observedAt }); return false; });
|
|
481
|
+
if (version.usage && windows)
|
|
482
|
+
version.usage = { ...version.usage, windows };
|
|
483
|
+
const reactive = stored.reactive.filter(r => { const w = reactiveUsageWindow(r); if (!canClear(w))
|
|
484
|
+
return true; cleared.push(w); return false; });
|
|
485
|
+
// Raw snapshots are also selection inputs; remove the cleared rejection there.
|
|
486
|
+
const matches = (w, fallback) => cleared.some(c => c.window === w.window && c.scope === w.scope && (w.observedAt ?? fallback) === c.observedAt);
|
|
487
|
+
if (version.passive && normalizePassiveUsage(version.passive.rateLimitInfo, version.passive.observedAt).windows.some(w => w.status === 'rejected' && matches(w, version.passive.observedAt))) {
|
|
488
|
+
// The rejected percentage is not a successful measurement. Keeping it
|
|
489
|
+
// as status-less usage could immediately re-block the cleared account.
|
|
490
|
+
delete version.passive;
|
|
491
|
+
}
|
|
492
|
+
if (version.probe?.usage)
|
|
493
|
+
version.probe.usage = { ...version.probe.usage, windows: version.probe.usage.windows.filter(w => w.status !== 'rejected' || !matches(w, version.probe.usage.observedAt)) };
|
|
494
|
+
await this.#save(this.#path(key, true), version);
|
|
495
|
+
await this.#save(this.#path(key, false), { reactive });
|
|
496
|
+
const markers = await this.#claimMarkers(key);
|
|
497
|
+
await Promise.all([...markers.keys()].filter(path => cleared.some(w => {
|
|
498
|
+
const prefix = join(this.#directory, 'claims', `${encodeURIComponent(key.accountId)}.${w.scope}.${w.window}.`);
|
|
499
|
+
const epoch = Number(path.slice(prefix.length, -'.claim'.length));
|
|
500
|
+
return path.startsWith(prefix) && epoch <= verificationEligibleAt(w, w.observedAt ?? observedAt);
|
|
501
|
+
})).map(path => rm(path, { force: true })));
|
|
502
|
+
for (const w of cleared)
|
|
503
|
+
this.#log(`verification cleared rejection window=${w.window}/${w.scope} count=${w.rejectionCount ?? 1}`);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
273
506
|
}
|
package/dist/types.d.ts
CHANGED