@teamvibe/poller 0.8.4-rc.1 → 0.8.4-rc.2
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.js +7 -4
- package/dist/pool-telemetry.d.ts +1 -1
- package/dist/pool-telemetry.js +5 -11
- package/dist/subscription-runtime.d.ts +1 -1
- package/dist/subscription-runtime.js +13 -5
- package/dist/subscription-selection.d.ts +7 -0
- package/dist/subscription-selection.js +48 -1
- package/dist/subscription-usage-store.d.ts +2 -0
- package/dist/subscription-usage-store.js +41 -20
- package/dist/usage-probe-job.js +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/claude-spawner.js
CHANGED
|
@@ -1588,7 +1588,9 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1588
1588
|
// fallback must report the fresh attempt's delivery flags independently.
|
|
1589
1589
|
if (!runtime?.resumePending)
|
|
1590
1590
|
sawSendMessageCall = false;
|
|
1591
|
-
const
|
|
1591
|
+
const spawned = await runClaudeCode(msg, sessionLog, effectiveCwd, directory, id, runtime?.resumePending ? false : first, lastMessageTs, onMessageSent, typingIndicator, runtime);
|
|
1592
|
+
const result = prepared?.account && spawned.rateLimitInfo?.status === 'rejected'
|
|
1593
|
+
? { ...spawned, success: false } : spawned;
|
|
1592
1594
|
sawSendMessageCall ||= result.sawSendMessageCall === true;
|
|
1593
1595
|
if (prepared?.account) {
|
|
1594
1596
|
if (poolAttempts.length >= 1000)
|
|
@@ -1622,7 +1624,7 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1622
1624
|
retainWorktree = false;
|
|
1623
1625
|
return result;
|
|
1624
1626
|
}
|
|
1625
|
-
if (result.quota && !result.killedByPoller) {
|
|
1627
|
+
if ((result.quota || result.rateLimitInfo?.status === 'rejected') && !result.killedByPoller) {
|
|
1626
1628
|
runtime.resumePending = true;
|
|
1627
1629
|
retainWorktree = true;
|
|
1628
1630
|
}
|
|
@@ -1645,7 +1647,7 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1645
1647
|
let switched = false;
|
|
1646
1648
|
const finishQuota = async (initial) => {
|
|
1647
1649
|
let result = initial;
|
|
1648
|
-
while (prepared?.account && result.quota && !result.killedByPoller && poolRuntime && runtime) {
|
|
1650
|
+
while (prepared?.account && (result.quota || result.rateLimitInfo?.status === 'rejected') && !result.killedByPoller && poolRuntime && runtime) {
|
|
1649
1651
|
attemptedAccounts.add(prepared.account.accountId);
|
|
1650
1652
|
if (poolRuntime.shouldStop() || attemptedAccounts.size >= 1000)
|
|
1651
1653
|
throw new PoolDeferredError();
|
|
@@ -1653,7 +1655,8 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1653
1655
|
logPoolFetch(sessionLog, prepared.pool);
|
|
1654
1656
|
if (prepared.pool.kind === 'ready')
|
|
1655
1657
|
await emitCandidateUsage(msg, prepared.pool.accounts, prepared.store);
|
|
1656
|
-
//
|
|
1658
|
+
// Quota and passive rejection evidence were persisted before selection.
|
|
1659
|
+
// The chain also excludes attempted accounts even if a concurrent observation replaces that evidence.
|
|
1657
1660
|
if (prepared.pool.kind === 'ready')
|
|
1658
1661
|
for (const account of prepared.pool.accounts) {
|
|
1659
1662
|
if (attemptedAccounts.has(account.accountId))
|
package/dist/pool-telemetry.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type StoredAccountUsage, type SubscriptionUsageStore } from './subscrip
|
|
|
3
3
|
import type { PoolAttempt } from './subscription-runtime.js';
|
|
4
4
|
import type { SessionMetrics, TeamVibeQueueMessage } from './types.js';
|
|
5
5
|
/** Explicit transport DTO: never serialize a pool account/credential envelope. */
|
|
6
|
-
export declare function accountUsageTelemetry(account: SubscriptionAccount, state: StoredAccountUsage): {
|
|
6
|
+
export declare function accountUsageTelemetry(account: SubscriptionAccount, state: StoredAccountUsage, reportedAt?: number): {
|
|
7
7
|
reactive: {
|
|
8
8
|
observedAt: number;
|
|
9
9
|
window: import("./claude-rate-limit.js").ClaudeQuotaWindow | undefined;
|
package/dist/pool-telemetry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { emitEvent } from './event-queue.js';
|
|
2
|
-
import {
|
|
3
|
-
import { safeQuota, safeRateLimitInfo, } from './subscription-usage-store.js';
|
|
2
|
+
import { reactiveEligibleAt } from './subscription-selection.js';
|
|
3
|
+
import { usageStateForSelection, safeQuota, safeRateLimitInfo, } from './subscription-usage-store.js';
|
|
4
4
|
function safeText(value) {
|
|
5
5
|
return value && value.length <= 128 && !/sk-ant-|Bearer\s|[\r\n]/i.test(value)
|
|
6
6
|
? value
|
|
@@ -30,18 +30,12 @@ function safeMetrics(metrics) {
|
|
|
30
30
|
return result;
|
|
31
31
|
}
|
|
32
32
|
/** Explicit transport DTO: never serialize a pool account/credential envelope. */
|
|
33
|
-
export function accountUsageTelemetry(account, state) {
|
|
34
|
-
const
|
|
35
|
-
? normalizePassiveUsage(state.passive.rateLimitInfo, state.passive.observedAt)
|
|
36
|
-
: undefined;
|
|
37
|
-
const probe = state.probe?.usage;
|
|
38
|
-
const usage = passive && (!probe || passive.observedAt >= probe.observedAt)
|
|
39
|
-
? passive
|
|
40
|
-
: probe;
|
|
33
|
+
export function accountUsageTelemetry(account, state, reportedAt = Date.now()) {
|
|
34
|
+
const { usage } = usageStateForSelection(state, reportedAt);
|
|
41
35
|
return {
|
|
42
36
|
accountId: account.accountId,
|
|
43
37
|
accountLabel: account.label ?? account.accountId,
|
|
44
|
-
reportedAt
|
|
38
|
+
reportedAt,
|
|
45
39
|
...(usage ? { usage } : {}),
|
|
46
40
|
...(state.probe
|
|
47
41
|
? {
|
|
@@ -71,5 +71,5 @@ export declare function preparePool(msg: TeamVibeQueueMessage, resolved: Resolve
|
|
|
71
71
|
}>;
|
|
72
72
|
export declare function accountPin(account: SubscriptionAccount, resolved: ResolvedPoolModel): SubscriptionPin;
|
|
73
73
|
export declare function validateAccount(account: SubscriptionAccount, resolved: ResolvedPoolModel): Promise<void>;
|
|
74
|
-
/** A snapshot TTL/recheck is not a provider reset.
|
|
74
|
+
/** A snapshot TTL/recheck is not a provider reset. Explicit rejections also establish resets. */
|
|
75
75
|
export declare function poolDeferredError(pool: PoolFetchResult<SubscriptionAccount>, resolved: ResolvedPoolModel, store: SubscriptionUsageStore, eligibleAt?: number, log?: PoolInfoLogger): Promise<PoolDeferredError>;
|
|
@@ -6,7 +6,7 @@ import { getPollerId } from './auth-provider.js';
|
|
|
6
6
|
import { fetchSubscriptionPool } from './subscription-pool-client.js';
|
|
7
7
|
import { selectPoolForResolvedModel } from './subscription-pool.js';
|
|
8
8
|
import { FileSubscriptionUsageStore, usageStateForSelection } from './subscription-usage-store.js';
|
|
9
|
-
import { PASSIVE_USAGE_TTL_MS, PROBE_USAGE_TTL_MS, reactiveEligibleAt, USAGE_THRESHOLD_PERCENT, POOL_RECHECK_MS } from './subscription-selection.js';
|
|
9
|
+
import { normalizePassiveUsage, selectSubscriptionAccount, PASSIVE_USAGE_TTL_MS, PROBE_USAGE_TTL_MS, reactiveEligibleAt, USAGE_THRESHOLD_PERCENT, POOL_RECHECK_MS } from './subscription-selection.js';
|
|
10
10
|
export class PoolDeferredError extends Error {
|
|
11
11
|
eligibleAt;
|
|
12
12
|
resetAt;
|
|
@@ -64,7 +64,7 @@ export async function validateAccount(account, resolved) {
|
|
|
64
64
|
throw new PoolDeferredError(selected.kind === 'deferred' ? selected.eligibleAt : undefined);
|
|
65
65
|
}
|
|
66
66
|
/** Message-only evidence; never changes selection, eligibility or durable pause state. */
|
|
67
|
-
async function poolUsageNotice(pool, store) {
|
|
67
|
+
async function poolUsageNotice(pool, resolved, store) {
|
|
68
68
|
if (pool.kind !== 'ready')
|
|
69
69
|
return {};
|
|
70
70
|
const now = Date.now();
|
|
@@ -75,7 +75,12 @@ async function poolUsageNotice(pool, store) {
|
|
|
75
75
|
const ttl = usage?.source === 'passive' ? PASSIVE_USAGE_TTL_MS : PROBE_USAGE_TTL_MS;
|
|
76
76
|
const windows = usage && usage.observedAt <= now && now < usage.observedAt + ttl
|
|
77
77
|
? usage.windows.filter(w => w.resetAt === undefined || w.resetAt > now) : [];
|
|
78
|
-
const
|
|
78
|
+
const rejection = selectSubscriptionAccount({
|
|
79
|
+
pool: { kind: 'ready', accounts: [account] },
|
|
80
|
+
states: { [account.accountId]: { usage: usage ? { ...usage, windows: usage.windows.filter(w => w.status === 'rejected') } : undefined } },
|
|
81
|
+
model: resolved.model, now,
|
|
82
|
+
});
|
|
83
|
+
const rejected = rejection.kind === 'deferred' || (state.reactive?.some(r => r.observedAt <= now && reactiveEligibleAt(r) > now) ?? false);
|
|
79
84
|
const blockers = windows.filter(w => w.percent !== undefined && w.percent >= (account.useFullQuota ? 100 : USAGE_THRESHOLD_PERCENT));
|
|
80
85
|
const resets = blockers.map(w => w.resetAt);
|
|
81
86
|
return {
|
|
@@ -94,9 +99,9 @@ async function poolUsageNotice(pool, store) {
|
|
|
94
99
|
return {};
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
|
-
/** A snapshot TTL/recheck is not a provider reset.
|
|
102
|
+
/** A snapshot TTL/recheck is not a provider reset. Explicit rejections also establish resets. */
|
|
98
103
|
export async function poolDeferredError(pool, resolved, store, eligibleAt, log) {
|
|
99
|
-
const notice = await poolUsageNotice(pool, store);
|
|
104
|
+
const notice = await poolUsageNotice(pool, resolved, store);
|
|
100
105
|
let usageDeferred = eligibleAt !== undefined;
|
|
101
106
|
const deferredError = (resetAt) => {
|
|
102
107
|
const error = new PoolDeferredError(eligibleAt, resetAt);
|
|
@@ -115,6 +120,9 @@ export async function poolDeferredError(pool, resolved, store, eligibleAt, log)
|
|
|
115
120
|
if (selected.kind !== 'deferred' || selected.eligibleAt !== eligibleAt)
|
|
116
121
|
continue;
|
|
117
122
|
const state = await store.read(account);
|
|
123
|
+
const usage = state.passive ? normalizePassiveUsage(state.passive.rateLimitInfo, state.passive.observedAt) : undefined;
|
|
124
|
+
if (usage?.windows.some(w => w.status === 'rejected' && w.resetAt === eligibleAt))
|
|
125
|
+
return deferredError(new Date(eligibleAt).toISOString());
|
|
118
126
|
const live = state.reactive.filter(r => !r.quota.resetAt || Date.parse(r.quota.resetAt) > Date.now());
|
|
119
127
|
if (live.length && live.every(r => r.quota.resetAt && Number.isFinite(Date.parse(r.quota.resetAt))) && live.some(r => Date.parse(r.quota.resetAt) === eligibleAt))
|
|
120
128
|
return deferredError(new Date(eligibleAt).toISOString());
|
|
@@ -5,6 +5,8 @@ export declare const USAGE_THRESHOLD_PERCENT = 90;
|
|
|
5
5
|
export declare const PASSIVE_USAGE_TTL_MS: number;
|
|
6
6
|
export declare const PROBE_USAGE_TTL_MS = 60000;
|
|
7
7
|
export declare const POOL_RECHECK_MS: number;
|
|
8
|
+
/** Safety hold from the rejection observation when the provider gives no reset. */
|
|
9
|
+
export declare const REJECTED_BLOCK_MS: number;
|
|
8
10
|
export interface AccountIdentity {
|
|
9
11
|
workspaceId: string;
|
|
10
12
|
accountId: string;
|
|
@@ -34,6 +36,9 @@ export interface UsageWindow {
|
|
|
34
36
|
window: 'five_hour' | 'seven_day';
|
|
35
37
|
scope: 'global' | 'sonnet' | 'opus' | 'haiku' | 'fable' | 'uncertain';
|
|
36
38
|
status?: 'allowed' | 'allowed_warning' | 'rejected';
|
|
39
|
+
/** Retained rejections keep their own clock when merged into newer measurements. */
|
|
40
|
+
observedAt?: number;
|
|
41
|
+
source?: 'passive' | 'probe';
|
|
37
42
|
percent?: number;
|
|
38
43
|
resetAt?: number;
|
|
39
44
|
}
|
|
@@ -42,6 +47,8 @@ export interface UsageObservation {
|
|
|
42
47
|
observedAt: number;
|
|
43
48
|
windows: readonly UsageWindow[];
|
|
44
49
|
}
|
|
50
|
+
/** Latest measurements plus durable rejections; omission or success never clears a hold. */
|
|
51
|
+
export declare function mergeUsageObservations(observations: readonly (UsageObservation | undefined)[], now: number): UsageObservation | undefined;
|
|
45
52
|
export interface ReactiveExhaustion {
|
|
46
53
|
observedAt: number;
|
|
47
54
|
quota: ClaudeQuotaFailure;
|
|
@@ -3,6 +3,40 @@ export const USAGE_THRESHOLD_PERCENT = 90;
|
|
|
3
3
|
export const PASSIVE_USAGE_TTL_MS = 5 * 60_000;
|
|
4
4
|
export const PROBE_USAGE_TTL_MS = 60_000;
|
|
5
5
|
export const POOL_RECHECK_MS = 15 * 60_000;
|
|
6
|
+
/** Safety hold from the rejection observation when the provider gives no reset. */
|
|
7
|
+
export const REJECTED_BLOCK_MS = 15 * 60_000;
|
|
8
|
+
/** Latest measurements plus durable rejections; omission or success never clears a hold. */
|
|
9
|
+
export function mergeUsageObservations(observations, now) {
|
|
10
|
+
let latest;
|
|
11
|
+
const rejected = new Map();
|
|
12
|
+
for (const usage of observations) {
|
|
13
|
+
if (!usage || usage.observedAt > now)
|
|
14
|
+
continue;
|
|
15
|
+
if (!latest || usage.observedAt >= latest.observedAt)
|
|
16
|
+
latest = usage;
|
|
17
|
+
for (const w of usage.windows) {
|
|
18
|
+
const observedAt = w.observedAt ?? usage.observedAt;
|
|
19
|
+
if (w.status !== 'rejected' || observedAt > now)
|
|
20
|
+
continue;
|
|
21
|
+
const key = `${w.window}:${w.scope}`;
|
|
22
|
+
const previous = rejected.get(key);
|
|
23
|
+
if (!previous || observedAt >= previous.observedAt)
|
|
24
|
+
rejected.set(key, { ...w, observedAt, source: w.source ?? usage.source });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!latest)
|
|
28
|
+
return undefined;
|
|
29
|
+
const active = [...rejected.values()].filter(w => now < (w.resetAt ?? w.observedAt + REJECTED_BLOCK_MS));
|
|
30
|
+
return {
|
|
31
|
+
...latest,
|
|
32
|
+
windows: [
|
|
33
|
+
...latest.windows.filter(w => w.status !== 'rejected' &&
|
|
34
|
+
(w.observedAt ?? latest.observedAt) === latest.observedAt &&
|
|
35
|
+
(w.source ?? latest.source) === latest.source).slice(0, 133 - active.length),
|
|
36
|
+
...active,
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
6
40
|
export function record(value) {
|
|
7
41
|
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
8
42
|
}
|
|
@@ -143,9 +177,22 @@ export function selectSubscriptionAccount(input) {
|
|
|
143
177
|
blockers.push(until);
|
|
144
178
|
}
|
|
145
179
|
const usage = state?.usage;
|
|
180
|
+
// A provider rejection outlives snapshot freshness. Never score it as headroom.
|
|
181
|
+
if (usage && usage.observedAt <= input.now) {
|
|
182
|
+
for (const w of usage.windows) {
|
|
183
|
+
if (w.status !== 'rejected' || !applicable(w.scope, input.model))
|
|
184
|
+
continue;
|
|
185
|
+
const observedAt = w.observedAt ?? usage.observedAt;
|
|
186
|
+
if (observedAt > input.now)
|
|
187
|
+
continue;
|
|
188
|
+
const until = w.resetAt ?? observedAt + REJECTED_BLOCK_MS;
|
|
189
|
+
if (input.now < until)
|
|
190
|
+
blockers.push(until);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
146
193
|
const expiry = usage ? usage.observedAt + (usage.source === 'passive' ? PASSIVE_USAGE_TTL_MS : PROBE_USAGE_TTL_MS) : 0;
|
|
147
194
|
const fresh = usage && usage.observedAt <= input.now && input.now < expiry;
|
|
148
|
-
const windows = fresh ? usage.windows.filter(w => applicable(w.scope, input.model) && (w.resetAt === undefined || w.resetAt > input.now)) : [];
|
|
195
|
+
const windows = fresh ? usage.windows.filter(w => w.status !== 'rejected' && (w.observedAt ?? usage.observedAt) === usage.observedAt && (w.source ?? usage.source) === usage.source && applicable(w.scope, input.model) && (w.resetAt === undefined || w.resetAt > input.now)) : [];
|
|
149
196
|
const uncertain = windows.some(w => w.scope === 'uncertain' || (w.scope !== 'global' && !family(input.model)));
|
|
150
197
|
for (const w of windows) {
|
|
151
198
|
if (w.percent !== undefined && w.percent >= (account.useFullQuota ? 100 : USAGE_THRESHOLD_PERCENT))
|
|
@@ -9,6 +9,8 @@ export interface ProbeObservation {
|
|
|
9
9
|
usage?: UsageObservation;
|
|
10
10
|
}
|
|
11
11
|
export interface StoredAccountUsage {
|
|
12
|
+
/** Latest measured snapshot plus active rejected windows, persisted across observations. */
|
|
13
|
+
usage?: UsageObservation;
|
|
12
14
|
passive?: {
|
|
13
15
|
rateLimitInfo: ClaudeRateLimitInfo;
|
|
14
16
|
observedAt: number;
|
|
@@ -2,12 +2,16 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { parseClaudeRateLimitInfo } from './claude-rate-limit.js';
|
|
5
|
-
import { normalizePassiveUsage, record, reactiveEligibleAt
|
|
5
|
+
import { normalizePassiveUsage, mergeUsageObservations, record, reactiveEligibleAt } from './subscription-selection.js';
|
|
6
6
|
export function usageStateForSelection(state, now) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
return {
|
|
8
|
+
usage: mergeUsageObservations([
|
|
9
|
+
state.usage,
|
|
10
|
+
state.passive ? normalizePassiveUsage(state.passive.rateLimitInfo, state.passive.observedAt) : undefined,
|
|
11
|
+
state.probe?.status === 'ok' ? state.probe.usage : undefined,
|
|
12
|
+
], now),
|
|
13
|
+
reactive: state.reactive,
|
|
14
|
+
};
|
|
11
15
|
}
|
|
12
16
|
export class SubscriptionUsageStoreError extends Error {
|
|
13
17
|
constructor() { super('Subscription usage store unavailable'); this.name = 'SubscriptionUsageStoreError'; }
|
|
@@ -51,6 +55,28 @@ export function safeQuota(quota) {
|
|
|
51
55
|
...(Number.isFinite(reset) && reset > 0 ? { resetAt: new Date(reset).toISOString() } : {}),
|
|
52
56
|
};
|
|
53
57
|
}
|
|
58
|
+
function safeUsage(value, source) {
|
|
59
|
+
const usage = record(value);
|
|
60
|
+
if (typeof usage['observedAt'] !== 'number' || !Array.isArray(usage['windows']))
|
|
61
|
+
throw new SubscriptionUsageStoreError();
|
|
62
|
+
validTime(usage['observedAt']);
|
|
63
|
+
const windows = usage['windows'].slice(0, 133).map(raw => {
|
|
64
|
+
const w = record(raw);
|
|
65
|
+
const window = w['window'] === 'five_hour' ? 'five_hour' : 'seven_day';
|
|
66
|
+
const scope = typeof w['scope'] === 'string' && ['global', 'sonnet', 'opus', 'haiku', 'fable'].includes(w['scope']) ? w['scope'] : 'uncertain';
|
|
67
|
+
const percent = typeof w['percent'] === 'number' && Number.isFinite(w['percent']) && w['percent'] >= 0 && w['percent'] <= 100 ? w['percent'] : undefined;
|
|
68
|
+
const resetAt = typeof w['resetAt'] === 'number' && Number.isFinite(w['resetAt']) && w['resetAt'] > 0 && w['resetAt'] <= 253402300799000 ? w['resetAt'] : undefined;
|
|
69
|
+
const status = w['status'] === 'allowed' || w['status'] === 'allowed_warning' || w['status'] === 'rejected' ? w['status'] : undefined;
|
|
70
|
+
const observedAt = typeof w['observedAt'] === 'number' ? w['observedAt'] : undefined;
|
|
71
|
+
if (observedAt !== undefined)
|
|
72
|
+
validTime(observedAt);
|
|
73
|
+
const source = w['source'] === 'passive' || w['source'] === 'probe' ? w['source'] : undefined;
|
|
74
|
+
return { window, scope, percent, resetAt, ...(status ? { status } : {}), ...(observedAt ? { observedAt } : {}), ...(source ? { source } : {}) };
|
|
75
|
+
});
|
|
76
|
+
if (usage['windows'].length > 133)
|
|
77
|
+
windows.push({ window: 'seven_day', scope: 'uncertain' });
|
|
78
|
+
return { source, observedAt: usage['observedAt'], windows };
|
|
79
|
+
}
|
|
54
80
|
function safeProbe(value) {
|
|
55
81
|
const obj = record(value);
|
|
56
82
|
const status = obj['status'];
|
|
@@ -62,21 +88,7 @@ function safeProbe(value) {
|
|
|
62
88
|
validTime(observedAt);
|
|
63
89
|
const result = { status: status, observedAt };
|
|
64
90
|
if (status === 'ok' && obj['usage']) {
|
|
65
|
-
|
|
66
|
-
if (typeof usage['observedAt'] !== 'number' || !Array.isArray(usage['windows']))
|
|
67
|
-
throw new SubscriptionUsageStoreError();
|
|
68
|
-
validTime(usage['observedAt']);
|
|
69
|
-
const windows = usage['windows'].slice(0, 133).map(raw => {
|
|
70
|
-
const w = record(raw);
|
|
71
|
-
const window = w['window'] === 'five_hour' ? 'five_hour' : 'seven_day';
|
|
72
|
-
const scope = typeof w['scope'] === 'string' && ['global', 'sonnet', 'opus', 'haiku', 'fable'].includes(w['scope']) ? w['scope'] : 'uncertain';
|
|
73
|
-
const percent = typeof w['percent'] === 'number' && Number.isFinite(w['percent']) && w['percent'] >= 0 && w['percent'] <= 100 ? w['percent'] : undefined;
|
|
74
|
-
const resetAt = typeof w['resetAt'] === 'number' && Number.isFinite(w['resetAt']) && w['resetAt'] > 0 && w['resetAt'] <= 253402300799000 ? w['resetAt'] : undefined;
|
|
75
|
-
return { window, scope, percent, resetAt };
|
|
76
|
-
});
|
|
77
|
-
if (usage['windows'].length > 133)
|
|
78
|
-
windows.push({ window: 'seven_day', scope: 'uncertain' });
|
|
79
|
-
result.usage = { source: 'probe', observedAt: usage['observedAt'], windows };
|
|
91
|
+
result.usage = safeUsage(obj['usage'], 'probe');
|
|
80
92
|
}
|
|
81
93
|
return result;
|
|
82
94
|
}
|
|
@@ -88,6 +100,9 @@ function safeVersion(value) {
|
|
|
88
100
|
}
|
|
89
101
|
if (value.probe)
|
|
90
102
|
state.probe = safeProbe(value.probe);
|
|
103
|
+
if (value.usage) {
|
|
104
|
+
state.usage = safeUsage(value.usage, value.usage.source === 'passive' ? 'passive' : 'probe');
|
|
105
|
+
}
|
|
91
106
|
return state;
|
|
92
107
|
}
|
|
93
108
|
function safeReactive(value) {
|
|
@@ -158,7 +173,9 @@ export class FileSubscriptionUsageStore {
|
|
|
158
173
|
const state = safeVersion(await this.#load(path, {}));
|
|
159
174
|
if (state.passive && state.passive.observedAt > observedAt && !options?.expireFutureObservations)
|
|
160
175
|
return;
|
|
176
|
+
const previous = usageStateForSelection({ ...state, reactive: [] }, observedAt).usage;
|
|
161
177
|
state.passive = { rateLimitInfo: safeRateLimitInfo(rateLimitInfo), observedAt };
|
|
178
|
+
state.usage = mergeUsageObservations([previous, normalizePassiveUsage(state.passive.rateLimitInfo, observedAt)], observedAt);
|
|
162
179
|
await this.#save(path, state);
|
|
163
180
|
});
|
|
164
181
|
}
|
|
@@ -216,7 +233,11 @@ export class FileSubscriptionUsageStore {
|
|
|
216
233
|
const path = this.#path(key, true);
|
|
217
234
|
await serial(path, async () => {
|
|
218
235
|
const state = safeVersion(await this.#load(path, {}));
|
|
236
|
+
// Network completion order must not discard a newer stored snapshot.
|
|
237
|
+
const mergeAt = Math.max(observation.observedAt, state.usage?.observedAt ?? 0, state.passive?.observedAt ?? 0);
|
|
238
|
+
const previous = usageStateForSelection({ ...state, reactive: [] }, mergeAt).usage;
|
|
219
239
|
state.probe = safeProbe(observation);
|
|
240
|
+
state.usage = mergeUsageObservations([previous, state.probe.usage], mergeAt);
|
|
220
241
|
await this.#save(path, state);
|
|
221
242
|
});
|
|
222
243
|
}
|
package/dist/usage-probe-job.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdtemp, rm, mkdir } from 'node:fs/promises';
|
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { normalizePassiveUsage } from './subscription-selection.js';
|
|
6
|
-
import { safeRateLimitInfo, } from './subscription-usage-store.js';
|
|
6
|
+
import { safeRateLimitInfo, usageStateForSelection, } from './subscription-usage-store.js';
|
|
7
7
|
import { parseClaudeRateLimitInfo, } from './claude-rate-limit.js';
|
|
8
8
|
export function isUsageProbe(value) {
|
|
9
9
|
return (!!value &&
|
|
@@ -176,7 +176,7 @@ export async function handleUsageProbe(job, options) {
|
|
|
176
176
|
...metadata,
|
|
177
177
|
accountLabel: account.label ?? account.accountId,
|
|
178
178
|
reportedAt: now,
|
|
179
|
-
usage,
|
|
179
|
+
usage: usageStateForSelection(await options.store.read(account), now).usage,
|
|
180
180
|
lastRateLimitInfo: safe,
|
|
181
181
|
probeStatus: 'ok',
|
|
182
182
|
probeObservedAt: now,
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamvibe/poller",
|
|
3
|
-
"version": "0.8.4-rc.
|
|
3
|
+
"version": "0.8.4-rc.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@teamvibe/poller",
|
|
9
|
-
"version": "0.8.4-rc.
|
|
9
|
+
"version": "0.8.4-rc.2",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@aws-sdk/client-dynamodb": "3.1108.0",
|
|
12
12
|
"@aws-sdk/client-sqs": "3.1108.0",
|