@teamvibe/poller 0.8.4-rc.1 → 0.8.4-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/claude-rate-limit.d.ts +1 -1
- package/dist/claude-rate-limit.js +3 -1
- package/dist/claude-spawner.js +9 -6
- package/dist/pool-telemetry.d.ts +5 -3
- package/dist/pool-telemetry.js +6 -11
- package/dist/redact-text.d.ts +2 -0
- package/dist/redact-text.js +15 -0
- package/dist/startup-auth.d.ts +1 -1
- package/dist/startup-auth.js +2 -14
- package/dist/subscription-runtime.d.ts +1 -1
- package/dist/subscription-runtime.js +13 -5
- package/dist/subscription-selection.d.ts +9 -0
- package/dist/subscription-selection.js +76 -19
- package/dist/subscription-usage-store.d.ts +2 -0
- package/dist/subscription-usage-store.js +51 -21
- package/dist/usage-probe-job.js +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,23 +74,23 @@ All configuration is via environment variables (or `~/.teamvibe/.env`):
|
|
|
74
74
|
|----------|----------|---------|-------------|
|
|
75
75
|
| `TEAMVIBE_POLLER_TOKEN` | Yes | — | Your poller authentication token |
|
|
76
76
|
| `CLAUDE_CODE_OAUTH_TOKEN` | No | — | Claude Code OAuth token (from `claude setup-token`) |
|
|
77
|
-
| `EXPOSE_CLAUDE_TOKEN_TO_AGENT` | No | `
|
|
78
|
-
| `TEAMVIBE_CLAUDE_OAUTH_TOKEN` | Injected |
|
|
79
|
-
| `TEAMVIBE_POOL_ACCOUNT_ID` | Injected |
|
|
77
|
+
| `EXPOSE_CLAUDE_TOKEN_TO_AGENT` | No | `true` | Expose the session token to agent subprocesses by default; only `0`/`false`/`no`/`off` (case-insensitive) opt out. Unset or any other value is on |
|
|
78
|
+
| `TEAMVIBE_CLAUDE_OAUTH_TOKEN` | Injected | Session token | In the poller-injected env: the selected pool account's token or the poller's non-empty `CLAUDE_CODE_OAUTH_TOKEN`; unset on explicit opt-out or without a token. Inherited aliases are cleared. Claude settings can override this |
|
|
79
|
+
| `TEAMVIBE_POOL_ACCOUNT_ID` | Injected | Selected account ID | In the poller-injected env: the selected pool account ID (no secret); unset on explicit opt-out or without a pool account. Claude settings can override this |
|
|
80
80
|
| `TEAMVIBE_API_URL` | No | `https://poller.api.teamvibe.ai` | API endpoint |
|
|
81
81
|
| `MAX_CONCURRENT_SESSIONS` | No | `5` | Max parallel Claude sessions |
|
|
82
82
|
| `CLAUDE_CLI_PATH` | No | `claude` | Path to Claude Code CLI |
|
|
83
83
|
| `TEAMVIBE_DATA_DIR` | No | `~/.teamvibe` | Data directory for brains and state |
|
|
84
84
|
|
|
85
|
-
Set `EXPOSE_CLAUDE_TOKEN_TO_AGENT=
|
|
85
|
+
Agents can run nested Claude sessions on the current session's account by default, for both pool and non-pool sessions. Set `EXPOSE_CLAUDE_TOKEN_TO_AGENT=false` in the poller's environment to opt out (`0`, `no`, and `off` also disable it, case-insensitively); unset or any other value keeps it on. Claude CLI strips `CLAUDE_CODE_OAUTH_TOKEN` from Bash subprocess environments by name, so agents can restore it from the alias:
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
88
|
CLAUDE_CODE_OAUTH_TOKEN="$TEAMVIBE_CLAUDE_OAUTH_TOKEN" claude -p "…"
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
In the environment the poller injects, the alias and account ID follow the selected account on resume and failover;
|
|
91
|
+
In the environment the poller injects, the alias and account ID follow the selected account on resume and failover; with explicit opt-out, the poller leaves both variables unset. **This guarantee applies only to the injected process environment.** Claude CLI subsequently applies settings-file `env` entries: a brain's `CLAUDE_CONFIG_DIR/settings.json` can restore either variable when the flag is off or override it with a stale value when the flag is on. Project `.claude/settings.json` and `.claude/settings.local.json` can also override them. `settings.local.json` is a project source, not a user file next to `CLAUDE_CONFIG_DIR/settings.json`; recent CLI versions also read project-local settings from the main checkout for worktree sessions ([Claude settings sources](https://code.claude.com/docs/en/settings)). Before each spawn, the poller emits one warning listing the keys and files defining either variable in these settings sources, never their values. It never edits brain settings. Remove conflicting entries yourself to preserve the injected values in the agent's environment.
|
|
92
92
|
|
|
93
|
-
**Security trade-off:**
|
|
93
|
+
**Security trade-off:** by default, the agent and its subprocesses can read and use the token, bypassing Claude CLI's credential filtering. Opt out for agents you do not trust with that credential. With no settings override, nested sessions use the selected session account; this option adds no nested-session usage accounting. Poller-side Claude output logs, returned output/errors, typing status and fallback Slack replies redact the actual injected Claude token and alias values, plus assignments to either token variable name, whether exposure is enabled or explicitly disabled. This does not prevent agents from using credentials or sending them directly through their own tools.
|
|
94
94
|
|
|
95
95
|
## How It Works
|
|
96
96
|
|
|
@@ -9,7 +9,7 @@ export interface ClaudeRateLimitWindow {
|
|
|
9
9
|
export interface ClaudeRateLimitInfo extends ClaudeRateLimitWindow {
|
|
10
10
|
status?: string;
|
|
11
11
|
rateLimitType?: string;
|
|
12
|
-
unifiedWindows?: Partial<Record<
|
|
12
|
+
unifiedWindows?: Partial<Record<string, ClaudeRateLimitWindow>>;
|
|
13
13
|
overageStatus?: string;
|
|
14
14
|
overageDisabledReason?: string;
|
|
15
15
|
isUsingOverage?: boolean;
|
|
@@ -32,7 +32,9 @@ export function parseClaudeRateLimitInfo(value) {
|
|
|
32
32
|
const windows = record(obj['unifiedWindows']);
|
|
33
33
|
if (windows) {
|
|
34
34
|
const parsed = {};
|
|
35
|
-
for (const key of
|
|
35
|
+
for (const key of Object.keys(windows).slice(0, 128)) {
|
|
36
|
+
if (key.length > 128 || !/^(five_hour|seven_day)(?:_[a-z_]+)?$/.test(key))
|
|
37
|
+
continue;
|
|
36
38
|
const data = record(windows[key]);
|
|
37
39
|
if (data)
|
|
38
40
|
parsed[key] = windowInfo(data);
|
package/dist/claude-spawner.js
CHANGED
|
@@ -460,7 +460,7 @@ export function sweepProcessGroup(proc, graceMs, options = {}) {
|
|
|
460
460
|
* would look like if left uncapped.
|
|
461
461
|
*/
|
|
462
462
|
export function buildSpawnEnv(params) {
|
|
463
|
-
const exposeToken =
|
|
463
|
+
const exposeToken = !/^(0|false|no|off)$/i.test(params.processEnv['EXPOSE_CLAUDE_TOKEN_TO_AGENT'] ?? '');
|
|
464
464
|
const accountToken = params.subscriptionAccount?.credential.unwrap();
|
|
465
465
|
const sessionToken = accountToken ?? params.processEnv['CLAUDE_CODE_OAUTH_TOKEN'];
|
|
466
466
|
return {
|
|
@@ -513,7 +513,7 @@ export function buildSpawnEnv(params) {
|
|
|
513
513
|
// watched after the session (and worktree) is gone. See tv#265.
|
|
514
514
|
POLLER_BRAIN_PATH: params.brainPath,
|
|
515
515
|
// Claude strips its own token name from Bash subprocesses. This alias
|
|
516
|
-
//
|
|
516
|
+
// exposes the session token by default; the poller can explicitly opt out.
|
|
517
517
|
// Explicit undefined values also clear inherited/overlay aliases.
|
|
518
518
|
TEAMVIBE_CLAUDE_OAUTH_TOKEN: exposeToken ? sessionToken || undefined : undefined,
|
|
519
519
|
TEAMVIBE_POOL_ACCOUNT_ID: exposeToken ? params.subscriptionAccount?.accountId : undefined,
|
|
@@ -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;
|
|
@@ -31,10 +31,11 @@ export declare function poolRunTelemetry(attempts: readonly PoolAttempt[], quota
|
|
|
31
31
|
score?: number | undefined;
|
|
32
32
|
reason?: "headroom_per_hour" | undefined;
|
|
33
33
|
rateWindow?: {
|
|
34
|
-
window: "five_hour" | "seven_day";
|
|
35
34
|
scope: "opus" | "haiku" | "global" | "sonnet" | "fable" | "uncertain";
|
|
36
35
|
percent: number | undefined;
|
|
37
36
|
resetAt: number | undefined;
|
|
37
|
+
kind?: "seven_day_overage_included" | "legacy-uncertain-seven-day" | undefined;
|
|
38
|
+
window: "five_hour" | "seven_day";
|
|
38
39
|
} | undefined;
|
|
39
40
|
accountId: string;
|
|
40
41
|
accountLabel: string;
|
|
@@ -48,10 +49,11 @@ export declare function poolRunTelemetry(attempts: readonly PoolAttempt[], quota
|
|
|
48
49
|
score?: number | undefined;
|
|
49
50
|
reason?: "headroom_per_hour" | undefined;
|
|
50
51
|
rateWindow?: {
|
|
51
|
-
window: "five_hour" | "seven_day";
|
|
52
52
|
scope: "opus" | "haiku" | "global" | "sonnet" | "fable" | "uncertain";
|
|
53
53
|
percent: number | undefined;
|
|
54
54
|
resetAt: number | undefined;
|
|
55
|
+
kind?: "seven_day_overage_included" | "legacy-uncertain-seven-day" | undefined;
|
|
56
|
+
window: "five_hour" | "seven_day";
|
|
55
57
|
} | undefined;
|
|
56
58
|
accountId?: string | undefined;
|
|
57
59
|
accountLabel?: string | 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
|
? {
|
|
@@ -92,6 +86,7 @@ export function poolRunTelemetry(attempts, quotaDeferEpisodes = 0) {
|
|
|
92
86
|
reason: a.reason,
|
|
93
87
|
rateWindow: {
|
|
94
88
|
window: a.rateWindow.window,
|
|
89
|
+
...(a.rateWindow.kind ? { kind: a.rateWindow.kind } : {}),
|
|
95
90
|
scope: a.rateWindow.scope,
|
|
96
91
|
percent: a.rateWindow.percent,
|
|
97
92
|
resetAt: a.rateWindow.resetAt,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Redact credentials in diagnostics without initializing poller authentication. */
|
|
2
|
+
export function redactStartupText(text, configuredTokens = []) {
|
|
3
|
+
let safe = text
|
|
4
|
+
.replace(/https?:\/\/[^\s/@]+@/gi, 'https://[REDACTED]@')
|
|
5
|
+
.replace(/\b(?:Authorization\s*:\s*)?Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]')
|
|
6
|
+
.replace(/\bx-access-token\s*:\s*[^\s/@,;]+/gi, 'x-access-token:[REDACTED]')
|
|
7
|
+
// Cover both Claude's token name and the opt-in subprocess alias in
|
|
8
|
+
// shell assignments and JSON env diagnostics, even without a token list.
|
|
9
|
+
.replace(/(\b(?:CLAUDE_CODE_OAUTH_TOKEN|TEAMVIBE_CLAUDE_OAUTH_TOKEN)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s,;]+)/gi, '$1[REDACTED]');
|
|
10
|
+
for (const token of configuredTokens) {
|
|
11
|
+
if (token)
|
|
12
|
+
safe = safe.split(token).join('[REDACTED]');
|
|
13
|
+
}
|
|
14
|
+
return safe;
|
|
15
|
+
}
|
package/dist/startup-auth.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare class UnknownStartupFailureError extends Error {
|
|
|
7
7
|
constructor(attempts: number);
|
|
8
8
|
}
|
|
9
9
|
/** The only formatter used for startup retry and terminal failure logs. */
|
|
10
|
-
export
|
|
10
|
+
export { redactStartupText } from './redact-text.js';
|
|
11
11
|
export declare function startupFailureDetail(error: unknown, operation: string, configuredTokens?: readonly string[]): string;
|
|
12
12
|
export declare function isPermanentStartupFailure(error: unknown): boolean;
|
|
13
13
|
/** Returns false when shutdown cancels startup. Permanent errors propagate to the caller. */
|
package/dist/startup-auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { redactStartupText } from './redact-text.js';
|
|
1
2
|
import { AuthHttpError, StartupConfigurationError } from './auth-provider.js';
|
|
2
3
|
export class UnknownStartupFailureError extends Error {
|
|
3
4
|
attempts;
|
|
@@ -34,20 +35,7 @@ function isTransient(error) {
|
|
|
34
35
|
return false;
|
|
35
36
|
}
|
|
36
37
|
/** The only formatter used for startup retry and terminal failure logs. */
|
|
37
|
-
export
|
|
38
|
-
let safe = text
|
|
39
|
-
.replace(/https?:\/\/[^\s/@]+@/gi, 'https://[REDACTED]@')
|
|
40
|
-
.replace(/\b(?:Authorization\s*:\s*)?Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]')
|
|
41
|
-
.replace(/\bx-access-token\s*:\s*[^\s/@,;]+/gi, 'x-access-token:[REDACTED]')
|
|
42
|
-
// Cover both Claude's token name and the opt-in subprocess alias in
|
|
43
|
-
// shell assignments and JSON env diagnostics, even without a token list.
|
|
44
|
-
.replace(/(\b(?:CLAUDE_CODE_OAUTH_TOKEN|TEAMVIBE_CLAUDE_OAUTH_TOKEN)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s,;]+)/gi, '$1[REDACTED]');
|
|
45
|
-
for (const token of configuredTokens) {
|
|
46
|
-
if (token)
|
|
47
|
-
safe = safe.split(token).join('[REDACTED]');
|
|
48
|
-
}
|
|
49
|
-
return safe;
|
|
50
|
-
}
|
|
38
|
+
export { redactStartupText } from './redact-text.js';
|
|
51
39
|
export function startupFailureDetail(error, operation, configuredTokens = []) {
|
|
52
40
|
let detail;
|
|
53
41
|
if (error instanceof AuthHttpError)
|
|
@@ -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;
|
|
@@ -33,7 +35,12 @@ export type PoolFetchResult<A extends AccountIdentity = AccountIdentity> = {
|
|
|
33
35
|
export interface UsageWindow {
|
|
34
36
|
window: 'five_hour' | 'seven_day';
|
|
35
37
|
scope: 'global' | 'sonnet' | 'opus' | 'haiku' | 'fable' | 'uncertain';
|
|
38
|
+
/** CLI kind, or the server compatibility provenance for a legacy Fable allowance. */
|
|
39
|
+
kind?: 'seven_day_overage_included' | 'legacy-uncertain-seven-day';
|
|
36
40
|
status?: 'allowed' | 'allowed_warning' | 'rejected';
|
|
41
|
+
/** Retained rejections keep their own clock when merged into newer measurements. */
|
|
42
|
+
observedAt?: number;
|
|
43
|
+
source?: 'passive' | 'probe';
|
|
37
44
|
percent?: number;
|
|
38
45
|
resetAt?: number;
|
|
39
46
|
}
|
|
@@ -42,6 +49,8 @@ export interface UsageObservation {
|
|
|
42
49
|
observedAt: number;
|
|
43
50
|
windows: readonly UsageWindow[];
|
|
44
51
|
}
|
|
52
|
+
/** Latest measurements plus durable rejections; omission or success never clears a hold. */
|
|
53
|
+
export declare function mergeUsageObservations(observations: readonly (UsageObservation | undefined)[], now: number): UsageObservation | undefined;
|
|
45
54
|
export interface ReactiveExhaustion {
|
|
46
55
|
observedAt: number;
|
|
47
56
|
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
|
}
|
|
@@ -22,29 +56,24 @@ function date(value) {
|
|
|
22
56
|
const ms = Date.parse(value);
|
|
23
57
|
return Number.isFinite(ms) && ms > 0 ? ms : undefined;
|
|
24
58
|
}
|
|
59
|
+
/** Undocumented CLI stream kinds: only explicit model names and the measured Fable alias. */
|
|
60
|
+
function passiveWindowKind(kind) {
|
|
61
|
+
if (kind === 'seven_day_overage_included')
|
|
62
|
+
return { window: 'seven_day', scope: 'fable', kind };
|
|
63
|
+
const legacy = kind.match(/^(five_hour|seven_day)(?:_(sonnet|opus|haiku|fable))?$/);
|
|
64
|
+
return {
|
|
65
|
+
window: legacy?.[1] === 'five_hour' ? 'five_hour' : 'seven_day',
|
|
66
|
+
scope: legacy ? (legacy[2] ?? 'global') : 'uncertain',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
25
69
|
export function normalizePassiveUsage(value, observedAt) {
|
|
26
70
|
const info = record(value);
|
|
27
71
|
const windows = [];
|
|
28
|
-
const
|
|
29
|
-
for (const window of ['five_hour', 'seven_day']) {
|
|
30
|
-
const raw = unified[window];
|
|
31
|
-
if (raw == null)
|
|
32
|
-
continue;
|
|
33
|
-
const item = record(raw);
|
|
34
|
-
windows.push({ window, scope: 'global', percent: percent(item['utilization'], 1), resetAt: epoch(item['resetsAt']) });
|
|
35
|
-
}
|
|
36
|
-
const type = info['rateLimitType'];
|
|
37
|
-
if (typeof type === 'string') {
|
|
38
|
-
const legacy = type.match(/^(five_hour|seven_day)(?:_(sonnet|opus|haiku|fable))?$/);
|
|
39
|
-
const candidate = {
|
|
40
|
-
window: legacy?.[1] === 'five_hour' ? 'five_hour' : 'seven_day',
|
|
41
|
-
scope: legacy ? (legacy[2] ?? 'global') : 'uncertain',
|
|
42
|
-
percent: percent(info['utilization'], 1), resetAt: epoch(info['resetsAt']),
|
|
43
|
-
...(info['status'] === 'allowed' || info['status'] === 'allowed_warning' || info['status'] === 'rejected'
|
|
44
|
-
? { status: info['status'] } : {}),
|
|
45
|
-
};
|
|
72
|
+
const add = (candidate) => {
|
|
46
73
|
const existing = windows.find(w => w.window === candidate.window && w.scope === candidate.scope);
|
|
47
74
|
if (existing) {
|
|
75
|
+
if (candidate.kind !== undefined)
|
|
76
|
+
existing.kind = candidate.kind;
|
|
48
77
|
if (candidate.status !== undefined)
|
|
49
78
|
existing.status = candidate.status;
|
|
50
79
|
// Collapse the same quota while retaining conservative conflict evidence.
|
|
@@ -56,6 +85,21 @@ export function normalizePassiveUsage(value, observedAt) {
|
|
|
56
85
|
else {
|
|
57
86
|
windows.push(candidate);
|
|
58
87
|
}
|
|
88
|
+
};
|
|
89
|
+
for (const [kind, raw] of Object.entries(record(info['unifiedWindows'])).slice(0, 128)) {
|
|
90
|
+
if (raw == null)
|
|
91
|
+
continue;
|
|
92
|
+
const item = record(raw);
|
|
93
|
+
add({ ...passiveWindowKind(kind), percent: percent(item['utilization'], 1), resetAt: epoch(item['resetsAt']) });
|
|
94
|
+
}
|
|
95
|
+
const type = info['rateLimitType'];
|
|
96
|
+
if (typeof type === 'string') {
|
|
97
|
+
add({
|
|
98
|
+
...passiveWindowKind(type),
|
|
99
|
+
percent: percent(info['utilization'], 1), resetAt: epoch(info['resetsAt']),
|
|
100
|
+
...(info['status'] === 'allowed' || info['status'] === 'allowed_warning' || info['status'] === 'rejected'
|
|
101
|
+
? { status: info['status'] } : {}),
|
|
102
|
+
});
|
|
59
103
|
}
|
|
60
104
|
return { source: 'passive', observedAt, windows };
|
|
61
105
|
}
|
|
@@ -143,9 +187,22 @@ export function selectSubscriptionAccount(input) {
|
|
|
143
187
|
blockers.push(until);
|
|
144
188
|
}
|
|
145
189
|
const usage = state?.usage;
|
|
190
|
+
// A provider rejection outlives snapshot freshness. Never score it as headroom.
|
|
191
|
+
if (usage && usage.observedAt <= input.now) {
|
|
192
|
+
for (const w of usage.windows) {
|
|
193
|
+
if (w.status !== 'rejected' || !applicable(w.scope, input.model))
|
|
194
|
+
continue;
|
|
195
|
+
const observedAt = w.observedAt ?? usage.observedAt;
|
|
196
|
+
if (observedAt > input.now)
|
|
197
|
+
continue;
|
|
198
|
+
const until = w.resetAt ?? observedAt + REJECTED_BLOCK_MS;
|
|
199
|
+
if (input.now < until)
|
|
200
|
+
blockers.push(until);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
146
203
|
const expiry = usage ? usage.observedAt + (usage.source === 'passive' ? PASSIVE_USAGE_TTL_MS : PROBE_USAGE_TTL_MS) : 0;
|
|
147
204
|
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)) : [];
|
|
205
|
+
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
206
|
const uncertain = windows.some(w => w.scope === 'uncertain' || (w.scope !== 'global' && !family(input.model)));
|
|
150
207
|
for (const w of windows) {
|
|
151
208
|
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;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import { redactStartupText } from './redact-text.js';
|
|
1
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
3
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { join, resolve } from 'node:path';
|
|
4
5
|
import { parseClaudeRateLimitInfo } from './claude-rate-limit.js';
|
|
5
|
-
import { normalizePassiveUsage, record, reactiveEligibleAt
|
|
6
|
+
import { normalizePassiveUsage, mergeUsageObservations, record, reactiveEligibleAt } from './subscription-selection.js';
|
|
6
7
|
export function usageStateForSelection(state, now) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
return {
|
|
9
|
+
usage: mergeUsageObservations([
|
|
10
|
+
state.usage,
|
|
11
|
+
state.passive ? normalizePassiveUsage(state.passive.rateLimitInfo, state.passive.observedAt) : undefined,
|
|
12
|
+
state.probe?.status === 'ok' ? state.probe.usage : undefined,
|
|
13
|
+
], now),
|
|
14
|
+
reactive: state.reactive,
|
|
15
|
+
};
|
|
11
16
|
}
|
|
12
17
|
export class SubscriptionUsageStoreError extends Error {
|
|
13
18
|
constructor() { super('Subscription usage store unavailable'); this.name = 'SubscriptionUsageStoreError'; }
|
|
@@ -30,12 +35,20 @@ function validTime(now) {
|
|
|
30
35
|
if (typeof now !== 'number' || !Number.isFinite(now) || now <= 0 || now > 253402300799000)
|
|
31
36
|
throw new SubscriptionUsageStoreError();
|
|
32
37
|
}
|
|
38
|
+
const loggedUnknownRateLimitKinds = new Set();
|
|
39
|
+
const knownRateLimitKind = /^(?:(five_hour|seven_day)(?:_(sonnet|opus|haiku|fable))?|seven_day_overage_included)$/;
|
|
33
40
|
export function safeRateLimitInfo(info) {
|
|
34
41
|
const parsed = parseClaudeRateLimitInfo(info) ?? {};
|
|
35
42
|
// Never persist arbitrary status strings or incidental stream fields.
|
|
36
43
|
if (parsed.status !== 'allowed' && parsed.status !== 'allowed_warning' && parsed.status !== 'rejected')
|
|
37
44
|
delete parsed.status;
|
|
38
|
-
|
|
45
|
+
for (const kind of [parsed.rateLimitType, ...Object.keys(parsed.unifiedWindows ?? {})]) {
|
|
46
|
+
if (!kind || kind === 'uncertain' || knownRateLimitKind.test(kind) || !/^[a-z_]{1,128}$/.test(kind) || loggedUnknownRateLimitKinds.has(kind))
|
|
47
|
+
continue;
|
|
48
|
+
loggedUnknownRateLimitKinds.add(kind);
|
|
49
|
+
console.warn(redactStartupText(`[Pool] unknown rate limit kind=${kind}`));
|
|
50
|
+
}
|
|
51
|
+
if (parsed.rateLimitType && !knownRateLimitKind.test(parsed.rateLimitType))
|
|
39
52
|
parsed.rateLimitType = 'uncertain';
|
|
40
53
|
delete parsed.overageStatus;
|
|
41
54
|
delete parsed.overageDisabledReason;
|
|
@@ -51,6 +64,28 @@ export function safeQuota(quota) {
|
|
|
51
64
|
...(Number.isFinite(reset) && reset > 0 ? { resetAt: new Date(reset).toISOString() } : {}),
|
|
52
65
|
};
|
|
53
66
|
}
|
|
67
|
+
function safeUsage(value, source) {
|
|
68
|
+
const usage = record(value);
|
|
69
|
+
if (typeof usage['observedAt'] !== 'number' || !Array.isArray(usage['windows']))
|
|
70
|
+
throw new SubscriptionUsageStoreError();
|
|
71
|
+
validTime(usage['observedAt']);
|
|
72
|
+
const windows = usage['windows'].slice(0, 133).map(raw => {
|
|
73
|
+
const w = record(raw);
|
|
74
|
+
const window = w['window'] === 'five_hour' ? 'five_hour' : 'seven_day';
|
|
75
|
+
const scope = typeof w['scope'] === 'string' && ['global', 'sonnet', 'opus', 'haiku', 'fable'].includes(w['scope']) ? w['scope'] : 'uncertain';
|
|
76
|
+
const percent = typeof w['percent'] === 'number' && Number.isFinite(w['percent']) && w['percent'] >= 0 && w['percent'] <= 100 ? w['percent'] : undefined;
|
|
77
|
+
const resetAt = typeof w['resetAt'] === 'number' && Number.isFinite(w['resetAt']) && w['resetAt'] > 0 && w['resetAt'] <= 253402300799000 ? w['resetAt'] : undefined;
|
|
78
|
+
const status = w['status'] === 'allowed' || w['status'] === 'allowed_warning' || w['status'] === 'rejected' ? w['status'] : undefined;
|
|
79
|
+
const observedAt = typeof w['observedAt'] === 'number' ? w['observedAt'] : undefined;
|
|
80
|
+
if (observedAt !== undefined)
|
|
81
|
+
validTime(observedAt);
|
|
82
|
+
const source = w['source'] === 'passive' || w['source'] === 'probe' ? w['source'] : undefined;
|
|
83
|
+
return { window, scope, ...(w['kind'] === 'seven_day_overage_included' ? { kind: w['kind'] } : {}), percent, resetAt, ...(status ? { status } : {}), ...(observedAt ? { observedAt } : {}), ...(source ? { source } : {}) };
|
|
84
|
+
});
|
|
85
|
+
if (usage['windows'].length > 133)
|
|
86
|
+
windows.push({ window: 'seven_day', scope: 'uncertain' });
|
|
87
|
+
return { source, observedAt: usage['observedAt'], windows };
|
|
88
|
+
}
|
|
54
89
|
function safeProbe(value) {
|
|
55
90
|
const obj = record(value);
|
|
56
91
|
const status = obj['status'];
|
|
@@ -62,21 +97,7 @@ function safeProbe(value) {
|
|
|
62
97
|
validTime(observedAt);
|
|
63
98
|
const result = { status: status, observedAt };
|
|
64
99
|
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 };
|
|
100
|
+
result.usage = safeUsage(obj['usage'], 'probe');
|
|
80
101
|
}
|
|
81
102
|
return result;
|
|
82
103
|
}
|
|
@@ -88,6 +109,9 @@ function safeVersion(value) {
|
|
|
88
109
|
}
|
|
89
110
|
if (value.probe)
|
|
90
111
|
state.probe = safeProbe(value.probe);
|
|
112
|
+
if (value.usage) {
|
|
113
|
+
state.usage = safeUsage(value.usage, value.usage.source === 'passive' ? 'passive' : 'probe');
|
|
114
|
+
}
|
|
91
115
|
return state;
|
|
92
116
|
}
|
|
93
117
|
function safeReactive(value) {
|
|
@@ -158,7 +182,9 @@ export class FileSubscriptionUsageStore {
|
|
|
158
182
|
const state = safeVersion(await this.#load(path, {}));
|
|
159
183
|
if (state.passive && state.passive.observedAt > observedAt && !options?.expireFutureObservations)
|
|
160
184
|
return;
|
|
185
|
+
const previous = usageStateForSelection({ ...state, reactive: [] }, observedAt).usage;
|
|
161
186
|
state.passive = { rateLimitInfo: safeRateLimitInfo(rateLimitInfo), observedAt };
|
|
187
|
+
state.usage = mergeUsageObservations([previous, normalizePassiveUsage(state.passive.rateLimitInfo, observedAt)], observedAt);
|
|
162
188
|
await this.#save(path, state);
|
|
163
189
|
});
|
|
164
190
|
}
|
|
@@ -216,7 +242,11 @@ export class FileSubscriptionUsageStore {
|
|
|
216
242
|
const path = this.#path(key, true);
|
|
217
243
|
await serial(path, async () => {
|
|
218
244
|
const state = safeVersion(await this.#load(path, {}));
|
|
245
|
+
// Network completion order must not discard a newer stored snapshot.
|
|
246
|
+
const mergeAt = Math.max(observation.observedAt, state.usage?.observedAt ?? 0, state.passive?.observedAt ?? 0);
|
|
247
|
+
const previous = usageStateForSelection({ ...state, reactive: [] }, mergeAt).usage;
|
|
219
248
|
state.probe = safeProbe(observation);
|
|
249
|
+
state.usage = mergeUsageObservations([previous, state.probe.usage], mergeAt);
|
|
220
250
|
await this.#save(path, state);
|
|
221
251
|
});
|
|
222
252
|
}
|
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.3",
|
|
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.3",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@aws-sdk/client-dynamodb": "3.1108.0",
|
|
12
12
|
"@aws-sdk/client-sqs": "3.1108.0",
|