@teamvibe/poller 0.8.4 → 0.8.5
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 +4 -0
- package/dist/claude-spawner.js +21 -15
- package/dist/poller.js +14 -6
- package/dist/pool-logging.d.ts +5 -1
- package/dist/pool-logging.js +24 -1
- package/dist/subscription-pool-client.d.ts +5 -1
- package/dist/subscription-pool-client.js +29 -2
- package/dist/subscription-pool.d.ts +4 -1
- package/dist/subscription-pool.js +11 -4
- package/dist/subscription-runtime.d.ts +57 -10
- package/dist/subscription-runtime.js +37 -19
- package/dist/subscription-selection.d.ts +3 -1
- package/dist/subscription-selection.js +8 -5
- package/dist/usage-limit-messages.d.ts +2 -0
- package/dist/usage-limit-messages.js +8 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,3 +95,7 @@ In the environment the poller injects, the alias and account ID follow the selec
|
|
|
95
95
|
## How It Works
|
|
96
96
|
|
|
97
97
|
The poller authenticates with the TeamVibe API using your token, then continuously polls an SQS queue for incoming messages from Slack. When a message arrives, it spawns a Claude Code session with the appropriate brain (knowledge base) and streams the response back to Slack.
|
|
98
|
+
|
|
99
|
+
## Subscription usage freshness
|
|
100
|
+
|
|
101
|
+
Passive usage percentages remain usable lower bounds until each window's `resetAt`. Without a reset time, passive windows expire five minutes after observation. Probe windows retain their 60-second TTL. Reset windows are ignored; known accounts still rank before unknown accounts. A window at the account's threshold defers selection until its expiry (reset for passive windows, TTL fallback without a reset).
|
package/dist/claude-spawner.js
CHANGED
|
@@ -902,7 +902,7 @@ async function runClaudeCode(msg, sessionLog, cwd, directory, sessionId, isFirst
|
|
|
902
902
|
if (!runtime?.account && isQuotaPaused())
|
|
903
903
|
throw new QuotaPausedError();
|
|
904
904
|
if (runtime?.shouldStop())
|
|
905
|
-
throw new PoolDeferredError();
|
|
905
|
+
throw new PoolDeferredError('infrastructure', { suppressNotice: true });
|
|
906
906
|
const prompt = runtime?.resumePending ? 'Continue the pending work in this session from the last completed action. Do not repeat completed actions or messages.' : buildPrompt(msg, contextHint);
|
|
907
907
|
// teamvibe.ai#293 bug 2: decides plain-text vs. stream-json stdin. Empty
|
|
908
908
|
// `extraArgs`/single-chunk `stdinChunks` (the overwhelming majority of
|
|
@@ -1530,7 +1530,6 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1530
1530
|
}
|
|
1531
1531
|
const resumePending = poolRuntime?.pin?.resumePending === true;
|
|
1532
1532
|
let effectiveCwd = cwd;
|
|
1533
|
-
let loggingPool = false;
|
|
1534
1533
|
let retainWorktree = resumePending;
|
|
1535
1534
|
const poolAttempts = poolRuntime?.attempts?.slice(-1000) ?? [];
|
|
1536
1535
|
let worktreeOwner;
|
|
@@ -1547,7 +1546,7 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1547
1546
|
catch (err) {
|
|
1548
1547
|
if (resumePending) {
|
|
1549
1548
|
sessionLog.warn('[Worktree] Pending worktree recreation failed; retaining work for the next recheck');
|
|
1550
|
-
throw new PoolDeferredError();
|
|
1549
|
+
throw new PoolDeferredError('infrastructure', { error: err });
|
|
1551
1550
|
}
|
|
1552
1551
|
sessionLog.warn(`[Worktree] Failed to create worktree, falling back to main brain dir: ${err instanceof Error ? err.message : err}`);
|
|
1553
1552
|
}
|
|
@@ -1562,7 +1561,6 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1562
1561
|
effectiveSessionId = randomUUID();
|
|
1563
1562
|
}
|
|
1564
1563
|
const fetchedPool = poolRuntime ? poolRuntime.pool ?? await fetchRuntimePool(msg) : undefined;
|
|
1565
|
-
loggingPool = fetchedPool !== undefined && fetchedPool.kind !== 'no_accounts_configured';
|
|
1566
1564
|
if (fetchedPool)
|
|
1567
1565
|
logPoolFetch(sessionLog, fetchedPool);
|
|
1568
1566
|
const scoped = poolRuntime ? await fetchConfigForSpawn({ pollerId: getPollerId() || '', workspaceId: msg.teamvibe.workspaceId, channelId: msg.teamvibe.channelId }) : undefined;
|
|
@@ -1580,8 +1578,8 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1580
1578
|
try {
|
|
1581
1579
|
await poolRuntime.persist(id, { ...accountPin(prepared.account, resolved), effectiveCwd, claudeConfigDir, resumePending: runtime?.resumePending, sawSendMessageCall }, poolAttempts);
|
|
1582
1580
|
}
|
|
1583
|
-
catch {
|
|
1584
|
-
throw new PoolDeferredError();
|
|
1581
|
+
catch (error) {
|
|
1582
|
+
throw new PoolDeferredError('infrastructure', { error });
|
|
1585
1583
|
}
|
|
1586
1584
|
}
|
|
1587
1585
|
// Only a quota continuation shares delivery history. A fresh-session
|
|
@@ -1638,8 +1636,9 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1638
1636
|
if (id && poolRuntime)
|
|
1639
1637
|
writes.push(poolRuntime.persist(id, { ...accountPin(prepared.account, resolved), effectiveCwd, claudeConfigDir, resumePending: runtime?.resumePending, sawSendMessageCall }, poolAttempts));
|
|
1640
1638
|
const outcomes = await Promise.allSettled(writes);
|
|
1641
|
-
|
|
1642
|
-
|
|
1639
|
+
const failed = outcomes.find((outcome) => outcome.status === 'rejected');
|
|
1640
|
+
if (failed)
|
|
1641
|
+
throw new PoolDeferredError('infrastructure', { error: failed.reason });
|
|
1643
1642
|
}
|
|
1644
1643
|
return result;
|
|
1645
1644
|
};
|
|
@@ -1649,8 +1648,10 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1649
1648
|
let result = initial;
|
|
1650
1649
|
while (prepared?.account && (result.quota || result.rateLimitInfo?.status === 'rejected') && !result.killedByPoller && poolRuntime && runtime) {
|
|
1651
1650
|
attemptedAccounts.add(prepared.account.accountId);
|
|
1652
|
-
if (poolRuntime.shouldStop()
|
|
1653
|
-
throw new PoolDeferredError();
|
|
1651
|
+
if (poolRuntime.shouldStop())
|
|
1652
|
+
throw new PoolDeferredError('infrastructure', { suppressNotice: true });
|
|
1653
|
+
if (attemptedAccounts.size >= 1000)
|
|
1654
|
+
throw new PoolDeferredError('usage');
|
|
1654
1655
|
prepared.pool = await fetchRuntimePool(msg);
|
|
1655
1656
|
logPoolFetch(sessionLog, prepared.pool);
|
|
1656
1657
|
if (prepared.pool.kind === 'ready')
|
|
@@ -1664,11 +1665,18 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1664
1665
|
}
|
|
1665
1666
|
const remaining = prepared.pool.kind === 'ready' ? prepared.pool.accounts.filter(a => !attemptedAccounts.has(a.accountId)) : [];
|
|
1666
1667
|
const selected = await selectPoolForResolvedModel(resolved, { workspaceId: msg.teamvibe.workspaceId, pool: remaining.length && prepared.pool.kind === 'ready' ? { ...prepared.pool, accounts: remaining } : prepared.pool, store: prepared.store, now: Date.now(), log: sessionLog, logAllExhausted: remaining.length === 0 });
|
|
1667
|
-
if (selected.kind !== 'selected' || attemptedAccounts.has(selected.account.accountId))
|
|
1668
|
-
|
|
1668
|
+
if (selected.kind !== 'selected' || attemptedAccounts.has(selected.account.accountId)) {
|
|
1669
|
+
const cause = selected.kind === 'deferred' || selected.kind === 'selected' ? 'usage'
|
|
1670
|
+
: selected.kind === 'usage_unavailable' ? 'infrastructure' : 'platform';
|
|
1671
|
+
// Usage eligibility must include previously attempted accounts too.
|
|
1672
|
+
throw await poolDeferredError(cause, prepared.pool, resolved, prepared.store, {
|
|
1673
|
+
log: remaining.length ? sessionLog : undefined,
|
|
1674
|
+
error: selected.kind === 'usage_unavailable' ? selected.error : undefined,
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1669
1677
|
effectiveSessionId = result.attemptedSessionId ?? effectiveSessionId;
|
|
1670
1678
|
if (!effectiveSessionId)
|
|
1671
|
-
throw new PoolDeferredError();
|
|
1679
|
+
throw new PoolDeferredError('infrastructure');
|
|
1672
1680
|
const fromAccount = prepared.account;
|
|
1673
1681
|
poolInfo(sessionLog, `switch from=${poolLabel(fromAccount)} to=${poolLabel(selected.account)} session=${JSON.stringify(effectiveSessionId)}`);
|
|
1674
1682
|
prepared.account = selected.account;
|
|
@@ -1710,8 +1718,6 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1710
1718
|
}
|
|
1711
1719
|
catch (error) {
|
|
1712
1720
|
if (error instanceof PoolDeferredError) {
|
|
1713
|
-
if (loggingPool)
|
|
1714
|
-
poolInfo(sessionLog, `work deferred eligibleAt=${new Date(error.eligibleAt).toISOString()} providerResetAt=${error.resetAt ?? 'unknown'}`);
|
|
1715
1721
|
retainWorktree = true;
|
|
1716
1722
|
error.attempts = poolAttempts;
|
|
1717
1723
|
}
|
package/dist/poller.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isUsageProbe, handleUsageProbe } from './usage-probe-job.js';
|
|
2
2
|
import { fetchSubscriptionPool } from './subscription-pool-client.js';
|
|
3
3
|
import { formatPoolUsageLimitMessage } from './usage-limit-messages.js';
|
|
4
|
-
import { logPoolUsageWrite } from './pool-logging.js';
|
|
4
|
+
import { logPoolUsageWrite, poolInfo } from './pool-logging.js';
|
|
5
5
|
import { emitCandidateUsage, poolRunTelemetry } from './pool-telemetry.js';
|
|
6
6
|
import { executionDirectory, selectBrainDirectory } from './brain-directory.js';
|
|
7
7
|
import { config, POLLER_VERSION } from './config.js';
|
|
@@ -440,8 +440,8 @@ export async function runInboxLoop(threadId, selectedDirectory, queueMessage, se
|
|
|
440
440
|
try {
|
|
441
441
|
saveInboxContext(kbPath, inboxMsg);
|
|
442
442
|
}
|
|
443
|
-
catch {
|
|
444
|
-
throw new PoolDeferredError();
|
|
443
|
+
catch (error) {
|
|
444
|
+
throw new PoolDeferredError('infrastructure', { error });
|
|
445
445
|
}
|
|
446
446
|
const savedRuntime = readInboxRuntime(kbPath, threadId);
|
|
447
447
|
if (savedRuntime?.filename === message.filename && savedRuntime.pin.workspaceId === queueMessage.teamvibe.workspaceId && (!currentPin || currentPin.accountId === savedRuntime.pin.accountId)) {
|
|
@@ -478,7 +478,7 @@ export async function runInboxLoop(threadId, selectedDirectory, queueMessage, se
|
|
|
478
478
|
persist: async (id, pin, attempts) => {
|
|
479
479
|
pin = { ...pin, inboxFilename: message.filename };
|
|
480
480
|
if (!lockToken || await persistSessionId(id, pin) === false)
|
|
481
|
-
throw new PoolDeferredError();
|
|
481
|
+
throw new PoolDeferredError('infrastructure');
|
|
482
482
|
currentSessionId = id;
|
|
483
483
|
currentPin = pin;
|
|
484
484
|
saveInboxRuntime(kbPath, threadId, { sessionId: id, pin, filename: message.filename, attempts, quotaDeferEpisodes });
|
|
@@ -594,7 +594,7 @@ export async function runInboxLoop(threadId, selectedDirectory, queueMessage, se
|
|
|
594
594
|
context: { threadId, filename: message.filename, iteration, reason: describeFailureReason(lastKilledByPoller, lastExitCode) },
|
|
595
595
|
});
|
|
596
596
|
}
|
|
597
|
-
if (hasSlackContext && (decision.kind !== 'quota' || notifyQuota)) {
|
|
597
|
+
if (hasSlackContext && !(poolIteration && lastKilledByPoller === 'shutdown') && (decision.kind !== 'quota' || notifyQuota)) {
|
|
598
598
|
if (decision.kind === 'neutral' || decision.kind === 'quota') {
|
|
599
599
|
await sendSlackMessage(deliveryMsg, decision.text);
|
|
600
600
|
}
|
|
@@ -608,6 +608,13 @@ export async function runInboxLoop(threadId, selectedDirectory, queueMessage, se
|
|
|
608
608
|
catch (error) {
|
|
609
609
|
typing?.stop();
|
|
610
610
|
if (poolIteration && error instanceof PoolDeferredError) {
|
|
611
|
+
poolInfo(sessionLog, `work deferred cause=${error.cause} error=${error.error} eligibleAt=${new Date(error.eligibleAt).toISOString()} providerResetAt=${error.resetAt ?? 'unknown'}`);
|
|
612
|
+
if (error.suppressNotice || shuttingDown) {
|
|
613
|
+
// Any prior marker has expired; retain its notice claim for restart.
|
|
614
|
+
requeueMessage(kbPath, threadId, message.filename);
|
|
615
|
+
deferred = true;
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
611
618
|
quotaPaused = true;
|
|
612
619
|
deferred = true;
|
|
613
620
|
const pause = deferPool(threadId, error.eligibleAt, error.resetAt);
|
|
@@ -620,10 +627,11 @@ export async function runInboxLoop(threadId, selectedDirectory, queueMessage, se
|
|
|
620
627
|
if (hasSlackContext && pause.notice !== 'delivered') {
|
|
621
628
|
const reconcile = pause.notice === 'claimed' || pause.notice === 'ambiguous';
|
|
622
629
|
if (!reconcile && !setPoolNotice(threadId, pause.episodeId, 'claimed'))
|
|
623
|
-
throw new PoolDeferredError(
|
|
630
|
+
throw new PoolDeferredError('infrastructure');
|
|
624
631
|
try {
|
|
625
632
|
const notice = formatPoolUsageLimitMessage({
|
|
626
633
|
...error.notice,
|
|
634
|
+
cause: error.cause,
|
|
627
635
|
thresholdDeferred: error.notice?.thresholdDeferred && !error.attempts?.some(a => a.quota),
|
|
628
636
|
resetAt: pause.resetAt ? Date.parse(pause.resetAt) : error.notice?.resetAt,
|
|
629
637
|
timeZone: config.POLLER_TZ,
|
package/dist/pool-logging.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PoolFetchDiagnostics } from './subscription-pool-client.js';
|
|
1
2
|
import type { SessionLogger } from './logger.js';
|
|
2
3
|
import type { ClaudeRateLimitInfo } from './claude-rate-limit.js';
|
|
3
4
|
import { type AccountIdentity, type AccountUsageState, type PoolFetchResult, type SelectionOutcome } from './subscription-selection.js';
|
|
@@ -9,7 +10,10 @@ export declare function poolInfo(log: PoolInfoLogger, message: string): void;
|
|
|
9
10
|
export declare function poolLabel(account: {
|
|
10
11
|
label?: string;
|
|
11
12
|
}): string;
|
|
12
|
-
export declare function
|
|
13
|
+
export declare function poolErrorClass(error: unknown): string;
|
|
14
|
+
/** Allowlisted class/code only: caught messages and arbitrary names may contain secrets. */
|
|
15
|
+
export declare function poolErrorDiagnostic(error: unknown): string;
|
|
16
|
+
export declare function logPoolFetch(log: PoolInfoLogger, pool: PoolFetchResult & PoolFetchDiagnostics): void;
|
|
13
17
|
export declare function logPoolSelection<A extends LabeledAccount>(log: PoolInfoLogger, pool: PoolFetchResult<A>, states: Readonly<Record<string, AccountUsageState>>, model: string | undefined, now: number, selected: SelectionOutcome<A>, logAllExhausted?: boolean): void;
|
|
14
18
|
/** Log the real store promise without changing its rejection or retry behavior. */
|
|
15
19
|
export declare function logPoolUsageWrite(log: PoolInfoLogger, account: {
|
package/dist/pool-logging.js
CHANGED
|
@@ -11,13 +11,36 @@ export function poolLabel(account) {
|
|
|
11
11
|
const label = account.label;
|
|
12
12
|
return JSON.stringify(label && label.length <= 128 && !/sk-ant-|Bearer\s|[\r\n]/i.test(label) ? label : 'unlabeled');
|
|
13
13
|
}
|
|
14
|
+
const POOL_ERROR_CLASSES = new Set([
|
|
15
|
+
'Error', 'TypeError', 'RangeError', 'AbortError', 'TimeoutError', 'SubscriptionUsageStoreError',
|
|
16
|
+
'ConditionalCheckFailedException', 'AccessDeniedException', 'ExpiredTokenException', 'CredentialsProviderError',
|
|
17
|
+
'ThrottlingException', 'ProvisionedThroughputExceededException', 'ValidationException', 'ResourceNotFoundException',
|
|
18
|
+
]);
|
|
19
|
+
export function poolErrorClass(error) {
|
|
20
|
+
return error instanceof Error && POOL_ERROR_CLASSES.has(error.name) ? error.name : 'UnknownError';
|
|
21
|
+
}
|
|
22
|
+
/** Allowlisted class/code only: caught messages and arbitrary names may contain secrets. */
|
|
23
|
+
export function poolErrorDiagnostic(error) {
|
|
24
|
+
if (error === undefined)
|
|
25
|
+
return 'none';
|
|
26
|
+
const record = (value) => value && typeof value === 'object' ? value : {};
|
|
27
|
+
const outer = record(error);
|
|
28
|
+
const code = record(outer['cause'])['code'] ?? outer['code'];
|
|
29
|
+
if (typeof code === 'string' && (POOL_ERROR_CLASSES.has(code) || [
|
|
30
|
+
'UND_ERR_SOCKET', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT', 'UND_ERR_ABORTED',
|
|
31
|
+
'ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', 'EPIPE', 'ENETUNREACH', 'EHOSTUNREACH',
|
|
32
|
+
'ENOENT', 'EACCES', 'EPERM', 'ENOSPC', 'ENOTDIR', 'EIO',
|
|
33
|
+
].includes(code)))
|
|
34
|
+
return code;
|
|
35
|
+
return poolErrorClass(error);
|
|
36
|
+
}
|
|
14
37
|
export function logPoolFetch(log, pool) {
|
|
15
38
|
if (pool.kind === 'no_accounts_configured')
|
|
16
39
|
return;
|
|
17
40
|
// The API exposes usable members only; hidden disabled labels are unavailable.
|
|
18
41
|
const enabled = pool.kind === 'ready' ? pool.accounts.filter(a => a.enabled).length : pool.kind === 'fetch_unavailable' && pool.reason === 'no_usable_accounts' ? 0 : 'unknown';
|
|
19
42
|
const configured = pool.kind === 'ready' || pool.kind === 'fetch_unavailable' && pool.reason === 'no_usable_accounts' ? 'true' : 'unknown';
|
|
20
|
-
poolInfo(log, `fetch poolConfigured=${configured} enabled=${enabled} usable=${enabled} result=${pool.kind}${pool.kind === 'fetch_unavailable' ? ` reason=${pool.reason}` : ''}`);
|
|
43
|
+
poolInfo(log, `fetch poolConfigured=${configured} enabled=${enabled} usable=${enabled} result=${pool.kind}${pool.kind === 'fetch_unavailable' ? ` reason=${pool.reason}` : ''}${pool.cause ? ` cause=${pool.cause}` : ''} attempts=${pool.attempts ?? 1}`);
|
|
21
44
|
}
|
|
22
45
|
export function logPoolSelection(log, pool, states, model, now, selected, logAllExhausted = true) {
|
|
23
46
|
if (pool.kind !== 'ready')
|
|
@@ -29,7 +29,11 @@ export interface PoolClientOptions {
|
|
|
29
29
|
}
|
|
30
30
|
/** Deadline covers response headers AND body, even for an uncooperative adapter. */
|
|
31
31
|
export declare function boundedRequest<T>(work: (signal: AbortSignal) => Promise<T>, signal?: AbortSignal, timeoutMs?: number): Promise<T>;
|
|
32
|
+
export interface PoolFetchDiagnostics {
|
|
33
|
+
attempts?: number;
|
|
34
|
+
cause?: string;
|
|
35
|
+
}
|
|
32
36
|
/** Bounded, ephemeral raw body. Neither it nor caught errors leave this module. */
|
|
33
37
|
export declare function readSubscriptionJson(response: Response): Promise<unknown>;
|
|
34
38
|
/** No global config, logging, workspace fallback or credential cache. */
|
|
35
|
-
export declare function fetchSubscriptionPool(input: PoolFetchInput, options: PoolClientOptions): Promise<PoolFetchResult<SubscriptionAccount
|
|
39
|
+
export declare function fetchSubscriptionPool(input: PoolFetchInput, options: PoolClientOptions): Promise<PoolFetchResult<SubscriptionAccount> & PoolFetchDiagnostics>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
1
2
|
import { ACCOUNT_PROBE_FAILURE_REASONS } from './usage-probe-contract.js';
|
|
2
3
|
import { record } from './subscription-selection.js';
|
|
3
4
|
/** Unwrap only at the optional Claude probe or future child-spawn boundary. */
|
|
@@ -39,6 +40,17 @@ export async function boundedRequest(work, signal, timeoutMs = 15_000) {
|
|
|
39
40
|
controller.signal.removeEventListener('abort', onAbort);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
/** Only allow known transport codes/classes out; never expose error messages. */
|
|
44
|
+
function networkCause(error) {
|
|
45
|
+
const outer = record(error);
|
|
46
|
+
const code = record(outer['cause'])['code'] ?? outer['code'];
|
|
47
|
+
if (typeof code === 'string' && [
|
|
48
|
+
'UND_ERR_SOCKET', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT', 'UND_ERR_ABORTED',
|
|
49
|
+
'ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', 'EPIPE', 'ENETUNREACH', 'EHOSTUNREACH',
|
|
50
|
+
].includes(code))
|
|
51
|
+
return code;
|
|
52
|
+
return error instanceof Error && ['Error', 'TypeError', 'AbortError', 'TimeoutError'].includes(error.name) ? error.name : 'UnknownError';
|
|
53
|
+
}
|
|
42
54
|
/** Bounded, ephemeral raw body. Neither it nor caught errors leave this module. */
|
|
43
55
|
export async function readSubscriptionJson(response) {
|
|
44
56
|
const reader = response.body?.getReader();
|
|
@@ -88,6 +100,21 @@ export async function fetchSubscriptionPool(input, options) {
|
|
|
88
100
|
catch {
|
|
89
101
|
return { kind: 'fetch_unavailable', reason: 'configuration' };
|
|
90
102
|
}
|
|
103
|
+
for (let attempts = 1;; attempts++) {
|
|
104
|
+
const result = await fetchPoolAttempt(url, input, options);
|
|
105
|
+
const retryable = result.kind === 'fetch_unavailable' && (result.reason === 'network' ||
|
|
106
|
+
result.reason === 'http' && (result.status === 429 || (result.status !== undefined && result.status >= 500 && result.status <= 599)));
|
|
107
|
+
if (!retryable || attempts >= 3)
|
|
108
|
+
return attempts > 1 ? { ...result, attempts } : result;
|
|
109
|
+
try {
|
|
110
|
+
await delay(attempts === 1 ? 500 : 2_000, undefined, { signal: options.signal });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return { kind: 'fetch_unavailable', reason: 'cancelled', attempts };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function fetchPoolAttempt(url, input, options) {
|
|
91
118
|
try {
|
|
92
119
|
return await boundedRequest(async (signal) => {
|
|
93
120
|
const response = await (options.fetch ?? fetch)(url, {
|
|
@@ -144,7 +171,7 @@ export async function fetchSubscriptionPool(input, options) {
|
|
|
144
171
|
return { kind: 'unknown_pool_state' };
|
|
145
172
|
}, options.signal, options.timeoutMs);
|
|
146
173
|
}
|
|
147
|
-
catch {
|
|
148
|
-
return { kind: 'fetch_unavailable', reason: options.signal?.aborted ? 'cancelled' : 'network' };
|
|
174
|
+
catch (error) {
|
|
175
|
+
return { kind: 'fetch_unavailable', reason: options.signal?.aborted ? 'cancelled' : 'network', cause: networkCause(error) };
|
|
149
176
|
}
|
|
150
177
|
}
|
|
@@ -5,13 +5,16 @@ import { type SubscriptionUsageStore } from './subscription-usage-store.js';
|
|
|
5
5
|
export type ResolvedPoolModel = ReturnType<typeof resolveModel>;
|
|
6
6
|
export type ResolvedPoolSelection<A extends AccountIdentity> = (SelectionOutcome<A> | {
|
|
7
7
|
kind: 'usage_unavailable';
|
|
8
|
+
error: unknown;
|
|
8
9
|
}) & ResolvedPoolModel;
|
|
9
10
|
/**
|
|
10
11
|
* #405 integration seam: resolveModel ONCE before calling, then reuse the
|
|
11
12
|
* returned model/modelSource for --model. No fetch, probe, spawn or env fallback
|
|
12
13
|
* occurs here. Only no_accounts_configured permits the legacy env credential.
|
|
13
14
|
*/
|
|
14
|
-
export declare function selectPoolForResolvedModel<A extends AccountIdentity
|
|
15
|
+
export declare function selectPoolForResolvedModel<A extends AccountIdentity & {
|
|
16
|
+
label?: string;
|
|
17
|
+
}>(resolvedModel: ResolvedPoolModel, input: {
|
|
15
18
|
workspaceId: string;
|
|
16
19
|
pool: PoolFetchResult<A>;
|
|
17
20
|
store: SubscriptionUsageStore;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
import { logPoolSelection, poolInfo, poolLabel, poolErrorClass } from './pool-logging.js';
|
|
2
3
|
import { selectSubscriptionAccount } from './subscription-selection.js';
|
|
3
4
|
import { usageStateForSelection } from './subscription-usage-store.js';
|
|
4
5
|
/**
|
|
@@ -17,11 +18,17 @@ export async function selectPoolForResolvedModel(resolvedModel, input) {
|
|
|
17
18
|
const states = Object.create(null);
|
|
18
19
|
try {
|
|
19
20
|
await Promise.all(input.pool.accounts.map(async (account) => {
|
|
20
|
-
|
|
21
|
+
try {
|
|
22
|
+
states[account.accountId] = usageStateForSelection(await input.store.read(account), input.now);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
poolInfo(input.log ?? logger, `usage read account=${poolLabel(account)} result=failed errorClass=${poolErrorClass(error)}`);
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
21
28
|
}));
|
|
22
29
|
}
|
|
23
|
-
catch {
|
|
24
|
-
return { kind: 'usage_unavailable', ...resolvedModel };
|
|
30
|
+
catch (error) {
|
|
31
|
+
return { kind: 'usage_unavailable', error, ...resolvedModel };
|
|
25
32
|
}
|
|
26
33
|
const selected = selectSubscriptionAccount({ pool: input.pool, states, model: resolvedModel.model, now: input.now, currentAccountId: input.currentAccountId });
|
|
27
34
|
if (input.log)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { type PoolInfoLogger } from './pool-logging.js';
|
|
2
|
-
import { type SubscriptionAccount } from './subscription-pool-client.js';
|
|
2
|
+
import { type PoolFetchDiagnostics, type SubscriptionAccount } from './subscription-pool-client.js';
|
|
3
3
|
import { type ResolvedPoolModel } from './subscription-pool.js';
|
|
4
4
|
import { FileSubscriptionUsageStore, type SubscriptionUsageStore } from './subscription-usage-store.js';
|
|
5
5
|
import { type PoolFetchResult, type HeadroomScore } from './subscription-selection.js';
|
|
6
6
|
import type { SubscriptionPin, TeamVibeQueueMessage, SessionMetrics } from './types.js';
|
|
7
|
-
import type { PoolUsageLimitNotice } from './usage-limit-messages.js';
|
|
7
|
+
import type { PoolUsageLimitNotice, PoolDeferCause } from './usage-limit-messages.js';
|
|
8
8
|
import type { ClaudeRateLimitInfo } from './claude-rate-limit.js';
|
|
9
9
|
import type { ClaudeQuotaFailure } from './claude-quota.js';
|
|
10
10
|
import type { KilledByPoller } from './classify-failed-iteration.js';
|
|
@@ -23,12 +23,21 @@ export interface PoolAttempt extends Partial<HeadroomScore> {
|
|
|
23
23
|
quota?: ClaudeQuotaFailure;
|
|
24
24
|
metrics?: SessionMetrics;
|
|
25
25
|
}
|
|
26
|
+
export declare const POOL_UNAVAILABLE_RECHECK_MS = 60000;
|
|
26
27
|
export declare class PoolDeferredError extends Error {
|
|
27
|
-
readonly
|
|
28
|
-
readonly resetAt?: string | undefined;
|
|
28
|
+
readonly cause: PoolDeferCause;
|
|
29
29
|
attempts?: readonly PoolAttempt[];
|
|
30
|
-
notice?: PoolUsageLimitNotice
|
|
31
|
-
|
|
30
|
+
notice?: Omit<PoolUsageLimitNotice, 'cause'>;
|
|
31
|
+
readonly eligibleAt: number;
|
|
32
|
+
readonly resetAt?: string;
|
|
33
|
+
readonly suppressNotice: boolean;
|
|
34
|
+
readonly error: string;
|
|
35
|
+
constructor(cause: PoolDeferCause, options?: {
|
|
36
|
+
eligibleAt?: number;
|
|
37
|
+
resetAt?: string;
|
|
38
|
+
error?: unknown;
|
|
39
|
+
suppressNotice?: boolean;
|
|
40
|
+
});
|
|
32
41
|
}
|
|
33
42
|
export interface PoolRuntimeOptions {
|
|
34
43
|
pin?: SubscriptionPin;
|
|
@@ -41,11 +50,28 @@ export interface PoolRuntimeOptions {
|
|
|
41
50
|
shouldStop: () => boolean;
|
|
42
51
|
}
|
|
43
52
|
export declare function subscriptionUsageStore(): FileSubscriptionUsageStore;
|
|
44
|
-
export declare function fetchRuntimePool(msg: TeamVibeQueueMessage): Promise<PoolFetchResult<SubscriptionAccount
|
|
53
|
+
export declare function fetchRuntimePool(msg: TeamVibeQueueMessage): Promise<PoolFetchResult<SubscriptionAccount> & PoolFetchDiagnostics>;
|
|
45
54
|
export declare function preparePool(msg: TeamVibeQueueMessage, resolved: ResolvedPoolModel, pin?: SubscriptionPin, fetched?: PoolFetchResult<SubscriptionAccount>, log?: PoolInfoLogger): Promise<{
|
|
46
55
|
account: SubscriptionAccount;
|
|
47
56
|
store: FileSubscriptionUsageStore;
|
|
48
|
-
pool:
|
|
57
|
+
pool: {
|
|
58
|
+
kind: "probe_account_unavailable";
|
|
59
|
+
reason: import("./usage-probe-contract.js").AccountProbeFailureReason;
|
|
60
|
+
} | {
|
|
61
|
+
kind: "no_accounts_configured";
|
|
62
|
+
} | {
|
|
63
|
+
kind: "unknown_pool_state";
|
|
64
|
+
} | {
|
|
65
|
+
kind: "fetch_unauthorized";
|
|
66
|
+
status: 401 | 403;
|
|
67
|
+
} | {
|
|
68
|
+
kind: "fetch_unavailable";
|
|
69
|
+
reason: "configuration" | "http" | "network" | "invalid_response" | "cancelled" | "no_usable_accounts";
|
|
70
|
+
status?: number;
|
|
71
|
+
} | {
|
|
72
|
+
kind: "ready";
|
|
73
|
+
accounts: readonly SubscriptionAccount[];
|
|
74
|
+
};
|
|
49
75
|
selection: ({
|
|
50
76
|
kind: "selected";
|
|
51
77
|
account: SubscriptionAccount;
|
|
@@ -66,10 +92,31 @@ export declare function preparePool(msg: TeamVibeQueueMessage, resolved: Resolve
|
|
|
66
92
|
} | {
|
|
67
93
|
account: undefined;
|
|
68
94
|
store: FileSubscriptionUsageStore;
|
|
69
|
-
pool:
|
|
95
|
+
pool: {
|
|
96
|
+
kind: "probe_account_unavailable";
|
|
97
|
+
reason: import("./usage-probe-contract.js").AccountProbeFailureReason;
|
|
98
|
+
} | {
|
|
99
|
+
kind: "no_accounts_configured";
|
|
100
|
+
} | {
|
|
101
|
+
kind: "unknown_pool_state";
|
|
102
|
+
} | {
|
|
103
|
+
kind: "fetch_unauthorized";
|
|
104
|
+
status: 401 | 403;
|
|
105
|
+
} | {
|
|
106
|
+
kind: "fetch_unavailable";
|
|
107
|
+
reason: "configuration" | "http" | "network" | "invalid_response" | "cancelled" | "no_usable_accounts";
|
|
108
|
+
status?: number;
|
|
109
|
+
} | {
|
|
110
|
+
kind: "ready";
|
|
111
|
+
accounts: readonly SubscriptionAccount[];
|
|
112
|
+
};
|
|
70
113
|
selection: undefined;
|
|
71
114
|
}>;
|
|
72
115
|
export declare function accountPin(account: SubscriptionAccount, resolved: ResolvedPoolModel): SubscriptionPin;
|
|
73
116
|
export declare function validateAccount(account: SubscriptionAccount, resolved: ResolvedPoolModel): Promise<void>;
|
|
74
117
|
/** A snapshot TTL/recheck is not a provider reset. Explicit rejections also establish resets. */
|
|
75
|
-
export declare function poolDeferredError(pool: PoolFetchResult<SubscriptionAccount
|
|
118
|
+
export declare function poolDeferredError(cause: PoolDeferCause, pool: PoolFetchResult<SubscriptionAccount> & PoolFetchDiagnostics, resolved: ResolvedPoolModel, store: SubscriptionUsageStore, options?: {
|
|
119
|
+
eligibleAt?: number;
|
|
120
|
+
log?: PoolInfoLogger;
|
|
121
|
+
error?: unknown;
|
|
122
|
+
}): Promise<PoolDeferredError>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { poolInfo, poolLabel } from './pool-logging.js';
|
|
1
|
+
import { poolInfo, poolLabel, poolErrorDiagnostic } from './pool-logging.js';
|
|
2
2
|
import { emitCandidateUsage } from './pool-telemetry.js';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { config } from './config.js';
|
|
@@ -6,16 +6,23 @@ 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 { normalizePassiveUsage, selectSubscriptionAccount,
|
|
9
|
+
import { normalizePassiveUsage, selectSubscriptionAccount, usageWindowExpiresAt, reactiveEligibleAt, USAGE_THRESHOLD_PERCENT, POOL_RECHECK_MS } from './subscription-selection.js';
|
|
10
|
+
export const POOL_UNAVAILABLE_RECHECK_MS = 60_000;
|
|
10
11
|
export class PoolDeferredError extends Error {
|
|
11
|
-
|
|
12
|
-
resetAt;
|
|
12
|
+
cause;
|
|
13
13
|
attempts;
|
|
14
14
|
notice;
|
|
15
|
-
|
|
15
|
+
eligibleAt;
|
|
16
|
+
resetAt;
|
|
17
|
+
suppressNotice;
|
|
18
|
+
error;
|
|
19
|
+
constructor(cause, options = {}) {
|
|
16
20
|
super('Subscription pool unavailable; work retained');
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
21
|
+
this.cause = cause;
|
|
22
|
+
this.eligibleAt = cause === 'usage' ? options.eligibleAt ?? Date.now() + POOL_RECHECK_MS : Date.now() + POOL_UNAVAILABLE_RECHECK_MS;
|
|
23
|
+
this.resetAt = cause === 'usage' ? options.resetAt : undefined;
|
|
24
|
+
this.suppressNotice = options.suppressNotice === true;
|
|
25
|
+
this.error = options.error instanceof PoolDeferredError ? options.error.error : poolErrorDiagnostic(options.error);
|
|
19
26
|
}
|
|
20
27
|
}
|
|
21
28
|
export function subscriptionUsageStore() {
|
|
@@ -53,15 +60,25 @@ export async function preparePool(msg, resolved, pin, fetched, log) {
|
|
|
53
60
|
}
|
|
54
61
|
if (selected.kind === 'no_accounts_configured')
|
|
55
62
|
return { account: undefined, store, pool, selection: undefined };
|
|
56
|
-
|
|
63
|
+
const cause = selected.kind === 'deferred' ? 'usage' : selected.kind === 'usage_unavailable' ? 'infrastructure' : 'platform';
|
|
64
|
+
throw await poolDeferredError(cause, pool, resolved, store, {
|
|
65
|
+
eligibleAt: selected.kind === 'deferred' ? selected.eligibleAt : undefined,
|
|
66
|
+
log, error: selected.kind === 'usage_unavailable' ? selected.error : undefined,
|
|
67
|
+
});
|
|
57
68
|
}
|
|
58
69
|
export function accountPin(account, resolved) {
|
|
59
70
|
return { workspaceId: account.workspaceId, accountId: account.accountId, credentialVersion: account.credentialVersion, ...resolved };
|
|
60
71
|
}
|
|
61
72
|
export async function validateAccount(account, resolved) {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
const store = subscriptionUsageStore();
|
|
74
|
+
const pool = { kind: 'ready', accounts: [account] };
|
|
75
|
+
const selected = await selectPoolForResolvedModel(resolved, { workspaceId: account.workspaceId, pool, store, now: Date.now() });
|
|
76
|
+
if (selected.kind !== 'selected') {
|
|
77
|
+
throw await poolDeferredError(selected.kind === 'deferred' ? 'usage' : 'infrastructure', pool, resolved, store, {
|
|
78
|
+
eligibleAt: selected.kind === 'deferred' ? selected.eligibleAt : undefined,
|
|
79
|
+
error: selected.kind === 'usage_unavailable' ? selected.error : undefined,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
65
82
|
}
|
|
66
83
|
/** Message-only evidence; never changes selection, eligibility or durable pause state. */
|
|
67
84
|
async function poolUsageNotice(pool, resolved, store) {
|
|
@@ -72,9 +89,8 @@ async function poolUsageNotice(pool, resolved, store) {
|
|
|
72
89
|
const accounts = await Promise.all(pool.accounts.map(async (account) => {
|
|
73
90
|
const state = usageStateForSelection(await store.read(account), now);
|
|
74
91
|
const usage = state.usage;
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
? usage.windows.filter(w => w.resetAt === undefined || w.resetAt > now) : [];
|
|
92
|
+
const windows = usage && usage.observedAt <= now
|
|
93
|
+
? usage.windows.filter(w => now < usageWindowExpiresAt(usage, w)) : [];
|
|
78
94
|
const rejection = selectSubscriptionAccount({
|
|
79
95
|
pool: { kind: 'ready', accounts: [account] },
|
|
80
96
|
states: { [account.accountId]: { usage: usage ? { ...usage, windows: usage.windows.filter(w => w.status === 'rejected') } : undefined } },
|
|
@@ -100,17 +116,19 @@ async function poolUsageNotice(pool, resolved, store) {
|
|
|
100
116
|
}
|
|
101
117
|
}
|
|
102
118
|
/** A snapshot TTL/recheck is not a provider reset. Explicit rejections also establish resets. */
|
|
103
|
-
export async function poolDeferredError(pool, resolved, store,
|
|
119
|
+
export async function poolDeferredError(cause, pool, resolved, store, options = {}) {
|
|
120
|
+
const { log } = options;
|
|
121
|
+
let { eligibleAt } = options;
|
|
122
|
+
if (cause !== 'usage')
|
|
123
|
+
return new PoolDeferredError(cause, { error: options.error ?? (pool.kind === 'fetch_unavailable' && pool.cause ? { code: pool.cause } : undefined) });
|
|
104
124
|
const notice = await poolUsageNotice(pool, resolved, store);
|
|
105
|
-
let usageDeferred = eligibleAt !== undefined;
|
|
106
125
|
const deferredError = (resetAt) => {
|
|
107
|
-
const error = new PoolDeferredError(eligibleAt, resetAt);
|
|
108
|
-
error.notice = { ...notice,
|
|
126
|
+
const error = new PoolDeferredError('usage', { eligibleAt, resetAt });
|
|
127
|
+
error.notice = { ...notice, resetAt: resetAt ? Date.parse(resetAt) : notice.resetAt };
|
|
109
128
|
return error;
|
|
110
129
|
};
|
|
111
130
|
if (eligibleAt === undefined) {
|
|
112
131
|
const selected = await selectPoolForResolvedModel(resolved, { workspaceId: pool.kind === 'ready' ? pool.accounts[0]?.workspaceId ?? '' : '', pool, store, now: Date.now(), log });
|
|
113
|
-
usageDeferred = selected.kind === 'deferred';
|
|
114
132
|
eligibleAt = selected.kind === 'deferred' ? selected.eligibleAt : Date.now() + POOL_RECHECK_MS;
|
|
115
133
|
}
|
|
116
134
|
if (pool.kind === 'ready') {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AccountProbeFailureReason } from './usage-probe-contract.js';
|
|
2
2
|
import type { ClaudeQuotaFailure } from './claude-quota.js';
|
|
3
3
|
export declare const USAGE_THRESHOLD_PERCENT = 90;
|
|
4
|
-
/**
|
|
4
|
+
/** Fallback freshness for passive windows without a reset time. */
|
|
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;
|
|
@@ -49,6 +49,8 @@ export interface UsageObservation {
|
|
|
49
49
|
observedAt: number;
|
|
50
50
|
windows: readonly UsageWindow[];
|
|
51
51
|
}
|
|
52
|
+
/** Passive percentages remain lower bounds until reset; probes retain their short TTL. */
|
|
53
|
+
export declare function usageWindowExpiresAt(usage: UsageObservation, window: UsageWindow): number;
|
|
52
54
|
/** Latest measurements plus durable rejections; omission or success never clears a hold. */
|
|
53
55
|
export declare function mergeUsageObservations(observations: readonly (UsageObservation | undefined)[], now: number): UsageObservation | undefined;
|
|
54
56
|
export interface ReactiveExhaustion {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
export const USAGE_THRESHOLD_PERCENT = 90;
|
|
2
|
-
/**
|
|
2
|
+
/** Fallback freshness for passive windows without a reset time. */
|
|
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
6
|
/** Safety hold from the rejection observation when the provider gives no reset. */
|
|
7
7
|
export const REJECTED_BLOCK_MS = 15 * 60_000;
|
|
8
|
+
/** Passive percentages remain lower bounds until reset; probes retain their short TTL. */
|
|
9
|
+
export function usageWindowExpiresAt(usage, window) {
|
|
10
|
+
const ttlExpiry = usage.observedAt + (usage.source === 'passive' ? PASSIVE_USAGE_TTL_MS : PROBE_USAGE_TTL_MS);
|
|
11
|
+
return usage.source === 'passive' ? window.resetAt ?? ttlExpiry : Math.min(window.resetAt ?? ttlExpiry, ttlExpiry);
|
|
12
|
+
}
|
|
8
13
|
/** Latest measurements plus durable rejections; omission or success never clears a hold. */
|
|
9
14
|
export function mergeUsageObservations(observations, now) {
|
|
10
15
|
let latest;
|
|
@@ -200,13 +205,11 @@ export function selectSubscriptionAccount(input) {
|
|
|
200
205
|
blockers.push(until);
|
|
201
206
|
}
|
|
202
207
|
}
|
|
203
|
-
const
|
|
204
|
-
const fresh = usage && usage.observedAt <= input.now && input.now < expiry;
|
|
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)) : [];
|
|
208
|
+
const windows = usage && usage.observedAt <= input.now ? usage.windows.filter(w => w.status !== 'rejected' && (w.observedAt ?? usage.observedAt) === usage.observedAt && (w.source ?? usage.source) === usage.source && applicable(w.scope, input.model) && input.now < usageWindowExpiresAt(usage, w)) : [];
|
|
206
209
|
const uncertain = windows.some(w => w.scope === 'uncertain' || (w.scope !== 'global' && !family(input.model)));
|
|
207
210
|
for (const w of windows) {
|
|
208
211
|
if (w.percent !== undefined && w.percent >= (account.useFullQuota ? 100 : USAGE_THRESHOLD_PERCENT))
|
|
209
|
-
blockers.push(
|
|
212
|
+
blockers.push(usageWindowExpiresAt(usage, w));
|
|
210
213
|
}
|
|
211
214
|
if (blockers.length) {
|
|
212
215
|
deadlines.push(Math.max(...blockers));
|
|
@@ -13,7 +13,9 @@ export interface UsageLimitAccount {
|
|
|
13
13
|
}[];
|
|
14
14
|
rejected?: boolean;
|
|
15
15
|
}
|
|
16
|
+
export type PoolDeferCause = 'usage' | 'platform' | 'infrastructure';
|
|
16
17
|
export interface PoolUsageLimitNotice extends UsageResetTiming {
|
|
18
|
+
cause: PoolDeferCause;
|
|
17
19
|
thresholdDeferred?: boolean;
|
|
18
20
|
accounts?: readonly UsageLimitAccount[];
|
|
19
21
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { USAGE_THRESHOLD_PERCENT } from './subscription-selection.js';
|
|
1
2
|
/** Owner-facing account policy location. */
|
|
2
3
|
export const FULL_QUOTA_SETTINGS_PATH = 'Connections (account menu → Use until exhausted)';
|
|
3
4
|
export function formatUsageReset(input) {
|
|
@@ -24,6 +25,12 @@ export function formatUsageReset(input) {
|
|
|
24
25
|
return `${time} (in ${relative})`;
|
|
25
26
|
}
|
|
26
27
|
export function formatPoolUsageLimitMessage(input) {
|
|
28
|
+
if (input.cause === 'infrastructure') {
|
|
29
|
+
return "⏳ Something went wrong while starting your session. Your message is saved and I'll retry in about a minute.";
|
|
30
|
+
}
|
|
31
|
+
if (input.cause === 'platform') {
|
|
32
|
+
return "⏳ I couldn't reach the TeamVibe platform to pick a Claude account. Your message is saved and I'll retry in about a minute.";
|
|
33
|
+
}
|
|
27
34
|
const accounts = input.accounts ?? [];
|
|
28
35
|
const headroom = accounts.filter(account => !account.useFullQuota && !account.rejected).map(account => {
|
|
29
36
|
const percentages = account.windows.map(w => w.percent).filter((p) => p !== undefined && Number.isFinite(p) && p >= 0 && p <= 100);
|
|
@@ -34,7 +41,7 @@ export function formatPoolUsageLimitMessage(input) {
|
|
|
34
41
|
const reset = formatUsageReset(input);
|
|
35
42
|
if (threshold) {
|
|
36
43
|
const labels = headroom.filter(a => a.label && a.remaining > 0).map(a => `${a.label} ${Number(a.remaining.toFixed(2))} %`).join(', ');
|
|
37
|
-
return `⏳ All Claude accounts in this workspace are above
|
|
44
|
+
return `⏳ All Claude accounts in this workspace are above ${USAGE_THRESHOLD_PERCENT} % of their usage limit, so I'm waiting for the next reset${reset ? ` at ${reset}` : ''}. Your message is saved and will run automatically.\n💡 There is still some capacity left${labels ? ` (${labels})` : ''}. A workspace owner can allow the full quota per account in ${FULL_QUOTA_SETTINGS_PATH}.`;
|
|
38
45
|
}
|
|
39
46
|
return `⏳ All Claude accounts in this workspace have hit their usage limit. Your message is saved and will run automatically after ${reset ? `the earliest reset at ${reset}` : 'the next reset'}.`;
|
|
40
47
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamvibe/poller",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@teamvibe/poller",
|
|
9
|
-
"version": "0.8.
|
|
9
|
+
"version": "0.8.5",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@aws-sdk/client-dynamodb": "3.1108.0",
|
|
12
12
|
"@aws-sdk/client-sqs": "3.1108.0",
|