@teamvibe/poller 0.8.6-rc.1 → 0.8.6-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/dist/claude-spawner.js +21 -2
- package/dist/pool-logging.d.ts +14 -1
- package/dist/pool-logging.js +29 -13
- package/dist/pool-telemetry.d.ts +2 -0
- package/dist/pool-telemetry.js +20 -1
- package/dist/subscription-pool.d.ts +8 -2
- package/dist/subscription-pool.js +2 -2
- package/dist/subscription-runtime.d.ts +12 -0
- package/dist/subscription-runtime.js +6 -3
- package/dist/subscription-selection.d.ts +3 -1
- package/dist/subscription-selection.js +5 -4
- package/dist/usage-probe-job.d.ts +2 -0
- package/dist/usage-probe-job.js +5 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/claude-spawner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logPoolFetch, logPoolUsageWrite, poolInfo, poolLabel } from './pool-logging.js';
|
|
2
|
-
import { accountUsageTelemetry, emitCandidateUsage, poolRunTelemetry } from './pool-telemetry.js';
|
|
2
|
+
import { emitPoolSelection, accountUsageTelemetry, emitCandidateUsage, poolRunTelemetry } from './pool-telemetry.js';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { readFileSync, existsSync } from 'fs';
|
|
@@ -1046,6 +1046,8 @@ async function runClaudeCode(msg, sessionLog, cwd, directory, sessionId, isFirst
|
|
|
1046
1046
|
activeProcessesByThread.set(msg.thread_id, proc);
|
|
1047
1047
|
if (runtime?.account)
|
|
1048
1048
|
activeSubscriptionAccounts.set(proc, runtime.account);
|
|
1049
|
+
if (proc.pid !== undefined)
|
|
1050
|
+
runtime?.onSpawn?.();
|
|
1049
1051
|
for (const chunk of spawnStdinPlan.stdinChunks) {
|
|
1050
1052
|
proc.stdin?.write(chunk);
|
|
1051
1053
|
}
|
|
@@ -1579,7 +1581,18 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1579
1581
|
const prepared = poolRuntime ? await preparePool(msg, resolved, poolRuntime.pin, fetchedPool, sessionLog, { cliPath: config.CLAUDE_CLI_PATH, isAtCapacity, emit: emitEvent, log: line => sessionLog.info(line) }) : undefined;
|
|
1580
1582
|
const modelNormalizedFrom = scoped?.model === resolved.model && scoped?.modelSource === resolved.modelSource
|
|
1581
1583
|
? scoped?.modelNormalizedFrom : undefined;
|
|
1582
|
-
const runtime = poolRuntime ? {
|
|
1584
|
+
const runtime = poolRuntime ? {
|
|
1585
|
+
resolved, modelNormalizedFrom, claudeConfigDir, account: prepared?.account,
|
|
1586
|
+
shouldStop: poolRuntime.shouldStop,
|
|
1587
|
+
resumePending: fetchedPool?.kind !== 'no_accounts_configured' && poolRuntime.pin?.resumePending === true,
|
|
1588
|
+
onSpawn: () => {
|
|
1589
|
+
if (prepared?.selection) {
|
|
1590
|
+
emitPoolSelection(msg, prepared.selection);
|
|
1591
|
+
// Retries report each launch, but release the original pin only once.
|
|
1592
|
+
delete prepared.selection.switch;
|
|
1593
|
+
}
|
|
1594
|
+
},
|
|
1595
|
+
} : undefined;
|
|
1583
1596
|
let sawSendMessageCall = poolRuntime?.pin?.resumePending === true && poolRuntime.pin.sawSendMessageCall === true;
|
|
1584
1597
|
const attempt = async (id, first) => {
|
|
1585
1598
|
if (prepared?.account && id && poolRuntime) {
|
|
@@ -1689,6 +1702,12 @@ export async function spawnPoolClaudeCode(msg, sessionLog, selectedDirectory, se
|
|
|
1689
1702
|
abortLocalProbe(selected.account);
|
|
1690
1703
|
poolInfo(sessionLog, `switch from=${poolLabel(fromAccount)} to=${poolLabel(selected.account)} session=${JSON.stringify(effectiveSessionId)}`);
|
|
1691
1704
|
prepared.account = selected.account;
|
|
1705
|
+
selected.candidates = [
|
|
1706
|
+
...(selected.candidates ?? []).filter(c => !attemptedAccounts.has(c.accountId)),
|
|
1707
|
+
...(prepared.pool.kind === 'ready' ? prepared.pool.accounts.filter(a => attemptedAccounts.has(a.accountId)).map(a => ({ accountId: a.accountId, accountLabel: JSON.parse(poolLabel(a)), outcome: 'skipped', reason: 'already exhausted in this session' })) : []),
|
|
1708
|
+
];
|
|
1709
|
+
if (prepared.pool.kind === 'ready' && prepared.pool.accounts.some(a => a.accountId === fromAccount.accountId))
|
|
1710
|
+
selected.switch = { fromAccountId: fromAccount.accountId, reason: 'reactive quota' };
|
|
1692
1711
|
prepared.selection = selected;
|
|
1693
1712
|
runtime.account = selected.account;
|
|
1694
1713
|
runtime.resumePending = true;
|
package/dist/pool-logging.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PoolFetchDiagnostics } from './subscription-pool-client.js';
|
|
2
2
|
import type { SessionLogger } from './logger.js';
|
|
3
3
|
import type { ClaudeRateLimitInfo } from './claude-rate-limit.js';
|
|
4
|
-
import { type AccountIdentity, type AccountUsageState, type PoolFetchResult, type SelectionOutcome } from './subscription-selection.js';
|
|
4
|
+
import { type AccountIdentity, type AccountUsageState, type PoolFetchResult, type SelectionOutcome, type UsageWindow } from './subscription-selection.js';
|
|
5
5
|
export type PoolInfoLogger = Pick<SessionLogger, 'info'>;
|
|
6
6
|
type LabeledAccount = AccountIdentity & {
|
|
7
7
|
label?: string;
|
|
@@ -14,7 +14,20 @@ export declare function poolErrorClass(error: unknown): string;
|
|
|
14
14
|
/** Allowlisted class/code only: caught messages and arbitrary names may contain secrets. */
|
|
15
15
|
export declare function poolErrorDiagnostic(error: unknown): string;
|
|
16
16
|
export declare function logPoolFetch(log: PoolInfoLogger, pool: PoolFetchResult & PoolFetchDiagnostics): void;
|
|
17
|
+
export interface PoolSelectionCandidate {
|
|
18
|
+
accountId: string;
|
|
19
|
+
accountLabel: string;
|
|
20
|
+
outcome: 'selected' | 'skipped';
|
|
21
|
+
reason: string;
|
|
22
|
+
score?: number;
|
|
23
|
+
headroom?: number;
|
|
24
|
+
rateWindow?: Pick<UsageWindow, 'window' | 'scope'>;
|
|
25
|
+
eligibleAt?: number;
|
|
26
|
+
}
|
|
27
|
+
/** The log and transport share a single explanation of the same selection snapshot. */
|
|
28
|
+
export declare function poolSelectionCandidates<A extends LabeledAccount>(pool: PoolFetchResult<A>, states: Readonly<Record<string, AccountUsageState>>, model: string | undefined, now: number, selected: SelectionOutcome<A>): PoolSelectionCandidate[];
|
|
17
29
|
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;
|
|
30
|
+
export declare function formatPoolUsageWindows(windows: readonly UsageWindow[], includeScope?: boolean): string;
|
|
18
31
|
/** Log the real store promise without changing its rejection or retry behavior. */
|
|
19
32
|
export declare function logPoolUsageWrite(log: PoolInfoLogger, account: {
|
|
20
33
|
label?: string;
|
package/dist/pool-logging.js
CHANGED
|
@@ -42,23 +42,36 @@ export function logPoolFetch(log, pool) {
|
|
|
42
42
|
const configured = pool.kind === 'ready' || pool.kind === 'fetch_unavailable' && pool.reason === 'no_usable_accounts' ? 'true' : 'unknown';
|
|
43
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}`);
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
/** The log and transport share a single explanation of the same selection snapshot. */
|
|
46
|
+
export function poolSelectionCandidates(pool, states, model, now, selected) {
|
|
46
47
|
if (pool.kind !== 'ready')
|
|
47
|
-
return;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
48
|
+
return [];
|
|
49
|
+
return pool.accounts.map(account => {
|
|
50
|
+
const base = { accountId: account.accountId, accountLabel: JSON.parse(poolLabel(account)), outcome: 'skipped' };
|
|
51
|
+
if (!account.enabled)
|
|
52
|
+
return { ...base, reason: 'disabled' };
|
|
53
53
|
const candidate = selectSubscriptionAccount({ pool: { ...pool, accounts: [account] }, states, model, now });
|
|
54
54
|
if (candidate.kind === 'deferred') {
|
|
55
55
|
const usageOnly = selectSubscriptionAccount({ pool: { ...pool, accounts: [account] }, states: { [account.accountId]: { usage: states[account.accountId]?.usage } }, model, now });
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
else if (candidate.kind === 'selected' && selected.kind === 'selected' && account.accountId !== selected.account.accountId) {
|
|
59
|
-
const reason = candidate.provenance === 'unknown' && selected.provenance === 'known' ? 'known observation preferred' : candidate.provenance === 'known' && selected.provenance === 'known' && candidate.score < selected.score ? 'lower headroom per hour' : 'deterministic account order';
|
|
60
|
-
poolInfo(log, `skipped account=${poolLabel(account)} reason=${reason}`);
|
|
56
|
+
return { ...base, reason: usageOnly.kind === 'deferred' ? `usage >=${account.useFullQuota ? 100 : 90}%` : 'reactive quota', eligibleAt: candidate.eligibleAt };
|
|
61
57
|
}
|
|
58
|
+
const metrics = candidate.kind === 'selected' && candidate.provenance === 'known'
|
|
59
|
+
? { score: candidate.score, headroom: (account.useFullQuota ? 100 : 90) - candidate.rateWindow.percent, rateWindow: { window: candidate.rateWindow.window, scope: candidate.rateWindow.scope } } : {};
|
|
60
|
+
if (selected.kind === 'selected' && account.accountId === selected.account.accountId)
|
|
61
|
+
return { ...base, ...metrics, outcome: 'selected', reason: 'selected' };
|
|
62
|
+
const reason = candidate.kind === 'selected' && selected.kind === 'selected'
|
|
63
|
+
? candidate.provenance === 'unknown' && selected.provenance === 'known' ? 'known observation preferred' : candidate.provenance === 'known' && selected.provenance === 'known' && candidate.score < selected.score ? 'lower headroom per hour' : 'deterministic account order'
|
|
64
|
+
: 'unavailable';
|
|
65
|
+
return { ...base, ...metrics, reason };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function logPoolSelection(log, pool, states, model, now, selected, logAllExhausted = true) {
|
|
69
|
+
if (pool.kind !== 'ready')
|
|
70
|
+
return;
|
|
71
|
+
for (const candidate of poolSelectionCandidates(pool, states, model, now, selected)) {
|
|
72
|
+
if (candidate.outcome !== 'skipped' || candidate.reason === 'unavailable')
|
|
73
|
+
continue;
|
|
74
|
+
poolInfo(log, `skipped account=${JSON.stringify(candidate.accountLabel)} reason=${candidate.reason}${candidate.eligibleAt === undefined ? '' : ` eligibleAt=${new Date(candidate.eligibleAt).toISOString()}`}`);
|
|
62
75
|
}
|
|
63
76
|
if (selected.kind === 'deferred' && logAllExhausted) {
|
|
64
77
|
// Ask the same policy about each observation so model scope, freshness and
|
|
@@ -87,9 +100,12 @@ export function logPoolSelection(log, pool, states, model, now, selected, logAll
|
|
|
87
100
|
poolInfo(log, `selected account=${poolLabel(selected.account)} model=${JSON.stringify(model ?? 'CLI default')} score=${score}`);
|
|
88
101
|
}
|
|
89
102
|
}
|
|
103
|
+
export function formatPoolUsageWindows(windows, includeScope = true) {
|
|
104
|
+
return windows.map(w => `${w.window}${includeScope ? `/${w.scope}` : ''}:${w.percent === undefined ? 'unknown' : `${Number(w.percent.toFixed(2))}%`}`).join(',') || 'none';
|
|
105
|
+
}
|
|
90
106
|
/** Log the real store promise without changing its rejection or retry behavior. */
|
|
91
107
|
export async function logPoolUsageWrite(log, account, info, write, source = 'passive') {
|
|
92
|
-
const windows = info ? normalizePassiveUsage(safeRateLimitInfo(info), Date.now()).windows
|
|
108
|
+
const windows = formatPoolUsageWindows(info ? normalizePassiveUsage(safeRateLimitInfo(info), Date.now()).windows : [], source === 'passive');
|
|
93
109
|
try {
|
|
94
110
|
await write();
|
|
95
111
|
poolInfo(log, `usage write account=${poolLabel(account)} result=ok windows=${windows || 'none'} source=${source}`);
|
package/dist/pool-telemetry.d.ts
CHANGED
|
@@ -60,3 +60,5 @@ export declare function poolRunTelemetry(attempts: readonly PoolAttempt[], quota
|
|
|
60
60
|
model?: string | undefined;
|
|
61
61
|
observedAt?: number | undefined;
|
|
62
62
|
};
|
|
63
|
+
/** Labels and explicit decision metrics only; never spread account or credential objects. */
|
|
64
|
+
export declare function emitPoolSelection(msg: TeamVibeQueueMessage, selection: import('./subscription-pool.js').ResolvedPoolSelection<SubscriptionAccount>): void;
|
package/dist/pool-telemetry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { emitEvent } from './event-queue.js';
|
|
2
|
-
import { reactiveEligibleAt } from './subscription-selection.js';
|
|
2
|
+
import { family, reactiveEligibleAt } from './subscription-selection.js';
|
|
3
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)
|
|
@@ -123,3 +123,22 @@ export function poolRunTelemetry(attempts, quotaDeferEpisodes = 0) {
|
|
|
123
123
|
attempts: safeAttempts,
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
|
+
// Keep ordering across retries and intervening threads even within one clock tick.
|
|
127
|
+
let lastSelectionAt = 0;
|
|
128
|
+
/** Labels and explicit decision metrics only; never spread account or credential objects. */
|
|
129
|
+
export function emitPoolSelection(msg, selection) {
|
|
130
|
+
if (selection.kind !== 'selected')
|
|
131
|
+
return;
|
|
132
|
+
lastSelectionAt = Math.max(Date.now(), lastSelectionAt + 1);
|
|
133
|
+
emitEvent('pool.selection', msg.teamvibe.channelId, {
|
|
134
|
+
workspaceId: msg.teamvibe.workspaceId,
|
|
135
|
+
threadId: msg.thread_id,
|
|
136
|
+
model: safeText(selection.model) ?? 'CLI default',
|
|
137
|
+
modelFamily: family(selection.model) ?? 'default',
|
|
138
|
+
selectedAccountId: selection.account.accountId,
|
|
139
|
+
selectedAccountLabel: safeText(selection.account.label) ?? 'unlabeled',
|
|
140
|
+
decidedAt: lastSelectionAt,
|
|
141
|
+
candidates: selection.candidates ?? [],
|
|
142
|
+
...(selection.switch ? { switch: selection.switch } : {}),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PoolInfoLogger } from './pool-logging.js';
|
|
1
|
+
import { type PoolSelectionCandidate, type PoolInfoLogger } from './pool-logging.js';
|
|
2
2
|
import type { resolveModel } from './claude-spawner.js';
|
|
3
3
|
import { type AccountIdentity, type PoolFetchResult, type SelectionOutcome } from './subscription-selection.js';
|
|
4
4
|
import { type SubscriptionUsageStore } from './subscription-usage-store.js';
|
|
@@ -6,7 +6,13 @@ export type ResolvedPoolModel = ReturnType<typeof resolveModel>;
|
|
|
6
6
|
export type ResolvedPoolSelection<A extends AccountIdentity> = (SelectionOutcome<A> | {
|
|
7
7
|
kind: 'usage_unavailable';
|
|
8
8
|
error: unknown;
|
|
9
|
-
}) & ResolvedPoolModel
|
|
9
|
+
}) & ResolvedPoolModel & {
|
|
10
|
+
candidates?: PoolSelectionCandidate[];
|
|
11
|
+
switch?: {
|
|
12
|
+
fromAccountId: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
10
16
|
/**
|
|
11
17
|
* #405 integration seam: resolveModel ONCE before calling, then reuse the
|
|
12
18
|
* returned model/modelSource for --model. No fetch, probe, spawn or env fallback
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger } from './logger.js';
|
|
2
|
-
import { logPoolSelection, poolInfo, poolLabel, poolErrorClass } from './pool-logging.js';
|
|
2
|
+
import { logPoolSelection, poolSelectionCandidates, poolInfo, poolLabel, poolErrorClass } from './pool-logging.js';
|
|
3
3
|
import { selectSubscriptionAccount } from './subscription-selection.js';
|
|
4
4
|
import { usageStateForSelection } from './subscription-usage-store.js';
|
|
5
5
|
/**
|
|
@@ -33,5 +33,5 @@ export async function selectPoolForResolvedModel(resolvedModel, input) {
|
|
|
33
33
|
const selected = selectSubscriptionAccount({ pool: input.pool, states, model: resolvedModel.model, now: input.now, currentAccountId: input.currentAccountId, currentAccountTolerance: input.currentAccountTolerance });
|
|
34
34
|
if (input.log)
|
|
35
35
|
logPoolSelection(input.log, input.pool, states, resolvedModel.model, input.now, selected, input.logAllExhausted);
|
|
36
|
-
return { ...selected, ...resolvedModel };
|
|
36
|
+
return { ...selected, ...resolvedModel, candidates: poolSelectionCandidates(input.pool, states, resolvedModel.model, input.now, selected) };
|
|
37
37
|
}
|
|
@@ -89,6 +89,12 @@ export declare function preparePool(msg: TeamVibeQueueMessage, resolved: Resolve
|
|
|
89
89
|
} & HeadroomScore & {
|
|
90
90
|
model: string | undefined;
|
|
91
91
|
modelSource: import("./claude-spawner.js").ModelSource;
|
|
92
|
+
} & {
|
|
93
|
+
candidates?: import("./pool-logging.js").PoolSelectionCandidate[];
|
|
94
|
+
switch?: {
|
|
95
|
+
fromAccountId: string;
|
|
96
|
+
reason: string;
|
|
97
|
+
};
|
|
92
98
|
}) | ({
|
|
93
99
|
kind: "selected";
|
|
94
100
|
account: SubscriptionAccount;
|
|
@@ -97,6 +103,12 @@ export declare function preparePool(msg: TeamVibeQueueMessage, resolved: Resolve
|
|
|
97
103
|
} & {
|
|
98
104
|
model: string | undefined;
|
|
99
105
|
modelSource: import("./claude-spawner.js").ModelSource;
|
|
106
|
+
} & {
|
|
107
|
+
candidates?: import("./pool-logging.js").PoolSelectionCandidate[];
|
|
108
|
+
switch?: {
|
|
109
|
+
fromAccountId: string;
|
|
110
|
+
reason: string;
|
|
111
|
+
};
|
|
100
112
|
});
|
|
101
113
|
} | {
|
|
102
114
|
account: undefined;
|
|
@@ -93,7 +93,7 @@ function probeUnknownAccounts(accounts, selectedAccountId, resolved, store, opti
|
|
|
93
93
|
poolInfo({ info: options.log }, `local probe account=${poolLabel(account)} result=cli_at_capacity`);
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
|
-
await runUsageProbe(account, { ...options, timeoutMs: LOCAL_PROBE_TIMEOUT_MS }, {
|
|
96
|
+
await runUsageProbe(account, { ...options, model: resolved.model, timeoutMs: LOCAL_PROBE_TIMEOUT_MS }, {
|
|
97
97
|
channelId, signal: probe.controller.signal,
|
|
98
98
|
onMeasurementComplete: () => {
|
|
99
99
|
if (localProbesInFlight.get(key) === probe)
|
|
@@ -123,10 +123,13 @@ export async function preparePool(msg, resolved, pin, fetched, log, probeOptions
|
|
|
123
123
|
const selected = await selectPoolForResolvedModel(resolved, { workspaceId: msg.teamvibe.workspaceId, pool, store, now: Date.now(), log, currentAccountId, currentAccountTolerance: PIN_HEADROOM_SWITCH_RELATIVE });
|
|
124
124
|
if (selected.kind === 'selected') {
|
|
125
125
|
abortLocalProbe(selected.account);
|
|
126
|
-
if (
|
|
126
|
+
if (pool.kind === 'ready' && currentAccountId && currentAccountId !== selected.account.accountId) {
|
|
127
127
|
const previous = pool.accounts.find(a => a.accountId === currentAccountId);
|
|
128
128
|
const previousSelection = previous ? await selectPoolForResolvedModel(resolved, { workspaceId: msg.teamvibe.workspaceId, pool: { kind: 'ready', accounts: [previous] }, store, now: Date.now() }) : undefined;
|
|
129
|
-
|
|
129
|
+
if (previous)
|
|
130
|
+
selected.switch = { fromAccountId: currentAccountId, reason: previousSelection?.kind === 'selected' ? 'better headroom' : 'pinned account unavailable' };
|
|
131
|
+
if (log)
|
|
132
|
+
poolInfo(log, `switch from=${poolLabel(previous ?? {})} to=${poolLabel(selected.account)} reason=${previousSelection?.kind === 'selected' ? 'better headroom' : 'pinned account unavailable'}`);
|
|
130
133
|
}
|
|
131
134
|
if (pool.kind === 'ready')
|
|
132
135
|
probeUnknownAccounts(pool.accounts, selected.account.accountId, resolved, store, {
|
|
@@ -5,6 +5,7 @@ export declare const PIN_HEADROOM_SWITCH_RELATIVE = 0.25;
|
|
|
5
5
|
export declare const USAGE_THRESHOLD_PERCENT = 90;
|
|
6
6
|
/** Fallback freshness for passive windows without a reset time. */
|
|
7
7
|
export declare const PASSIVE_USAGE_TTL_MS: number;
|
|
8
|
+
/** Fallback freshness for probe windows without a reset time. */
|
|
8
9
|
export declare const PROBE_USAGE_TTL_MS = 60000;
|
|
9
10
|
export declare const POOL_RECHECK_MS: number;
|
|
10
11
|
/** Safety hold from the rejection observation when the provider gives no reset. */
|
|
@@ -51,7 +52,7 @@ export interface UsageObservation {
|
|
|
51
52
|
observedAt: number;
|
|
52
53
|
windows: readonly UsageWindow[];
|
|
53
54
|
}
|
|
54
|
-
/** Passive percentages remain lower bounds until
|
|
55
|
+
/** Passive and probe percentages remain lower bounds until each window resets. */
|
|
55
56
|
export declare function usageWindowExpiresAt(usage: UsageObservation, window: UsageWindow): number;
|
|
56
57
|
/** Latest measurements plus durable rejections; omission or success never clears a hold. */
|
|
57
58
|
export declare function mergeUsageObservations(observations: readonly (UsageObservation | undefined)[], now: number): UsageObservation | undefined;
|
|
@@ -87,6 +88,7 @@ export type SelectionOutcome<A extends AccountIdentity = AccountIdentity> = ({
|
|
|
87
88
|
export declare function record(value: unknown): Record<string, unknown>;
|
|
88
89
|
export declare function normalizePassiveUsage(value: unknown, observedAt: number): UsageObservation;
|
|
89
90
|
export declare function normalizeProbeUsage(value: unknown, observedAt: number): UsageObservation;
|
|
91
|
+
export declare function family(model: string | undefined): 'sonnet' | 'opus' | 'haiku' | 'fable' | undefined;
|
|
90
92
|
export declare function reactiveEligibleAt(reactive: ReactiveExhaustion): number;
|
|
91
93
|
/** Pure policy. Unknown is eligible after every known/reactive blocker is ruled out. */
|
|
92
94
|
export declare function selectSubscriptionAccount<A extends AccountIdentity>(input: {
|
|
@@ -3,14 +3,15 @@ export const PIN_HEADROOM_SWITCH_RELATIVE = 0.25;
|
|
|
3
3
|
export const USAGE_THRESHOLD_PERCENT = 90;
|
|
4
4
|
/** Fallback freshness for passive windows without a reset time. */
|
|
5
5
|
export const PASSIVE_USAGE_TTL_MS = 5 * 60_000;
|
|
6
|
+
/** Fallback freshness for probe windows without a reset time. */
|
|
6
7
|
export const PROBE_USAGE_TTL_MS = 60_000;
|
|
7
8
|
export const POOL_RECHECK_MS = 15 * 60_000;
|
|
8
9
|
/** Safety hold from the rejection observation when the provider gives no reset. */
|
|
9
10
|
export const REJECTED_BLOCK_MS = 15 * 60_000;
|
|
10
|
-
/** Passive percentages remain lower bounds until
|
|
11
|
+
/** Passive and probe percentages remain lower bounds until each window resets. */
|
|
11
12
|
export function usageWindowExpiresAt(usage, window) {
|
|
12
13
|
const ttlExpiry = usage.observedAt + (usage.source === 'passive' ? PASSIVE_USAGE_TTL_MS : PROBE_USAGE_TTL_MS);
|
|
13
|
-
return
|
|
14
|
+
return window.resetAt ?? ttlExpiry;
|
|
14
15
|
}
|
|
15
16
|
/** Latest measurements plus durable rejections; omission or success never clears a hold. */
|
|
16
17
|
export function mergeUsageObservations(observations, now) {
|
|
@@ -142,7 +143,7 @@ export function normalizeProbeUsage(value, observedAt) {
|
|
|
142
143
|
}
|
|
143
144
|
return { source: 'probe', observedAt, windows };
|
|
144
145
|
}
|
|
145
|
-
function family(model) {
|
|
146
|
+
export function family(model) {
|
|
146
147
|
return model?.toLowerCase().match(/^(?:claude-)?(sonnet|opus|haiku|fable)(?:-\d[\w.-]*)?(?:\[1m\])?$/)?.[1];
|
|
147
148
|
}
|
|
148
149
|
function applicable(scope, model) {
|
|
@@ -208,7 +209,7 @@ export function selectSubscriptionAccount(input) {
|
|
|
208
209
|
}
|
|
209
210
|
}
|
|
210
211
|
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)) : [];
|
|
211
|
-
const uncertain = windows.some(w => w.scope === 'uncertain' || (w.scope !== 'global' && !family(input.model)));
|
|
212
|
+
const uncertain = windows.some(w => w.scope === 'uncertain' || (w.scope !== 'global' && input.model !== undefined && !family(input.model)));
|
|
212
213
|
for (const w of windows) {
|
|
213
214
|
if (w.percent !== undefined && w.percent >= (account.useFullQuota ? 100 : USAGE_THRESHOLD_PERCENT))
|
|
214
215
|
blockers.push(usageWindowExpiresAt(usage, w));
|
|
@@ -8,6 +8,8 @@ type ProbeResult = Reason | 'ok' | 'cancelled';
|
|
|
8
8
|
export interface UsageProbeOptions {
|
|
9
9
|
store: SubscriptionUsageStore;
|
|
10
10
|
cliPath: string;
|
|
11
|
+
/** Use the session's resolved model for local probes; platform jobs default to CLI behavior. */
|
|
12
|
+
model?: string;
|
|
11
13
|
timeoutMs?: number;
|
|
12
14
|
/** Injectable child for deterministic tests; production uses node spawn. */
|
|
13
15
|
spawn?: typeof spawn;
|
package/dist/usage-probe-job.js
CHANGED
|
@@ -5,7 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { normalizePassiveUsage } from './subscription-selection.js';
|
|
6
6
|
import { safeRateLimitInfo, usageStateForSelection, } from './subscription-usage-store.js';
|
|
7
7
|
import { parseClaudeRateLimitInfo, } from './claude-rate-limit.js';
|
|
8
|
-
import { poolInfo, poolLabel } from './pool-logging.js';
|
|
8
|
+
import { formatPoolUsageWindows, poolInfo, poolLabel } from './pool-logging.js';
|
|
9
9
|
export function isUsageProbe(value) {
|
|
10
10
|
return (!!value &&
|
|
11
11
|
typeof value === 'object' &&
|
|
@@ -84,6 +84,7 @@ export async function handleUsageProbe(job, options) {
|
|
|
84
84
|
export async function runUsageProbe(account, options, context = {}) {
|
|
85
85
|
let result = 'unknown';
|
|
86
86
|
let cwd;
|
|
87
|
+
let measuredWindows;
|
|
87
88
|
const cancelled = () => context.signal?.aborted === true;
|
|
88
89
|
try {
|
|
89
90
|
if (cancelled())
|
|
@@ -109,6 +110,7 @@ export async function runUsageProbe(account, options, context = {}) {
|
|
|
109
110
|
const child = (options.spawn ?? spawn)(options.cliPath, [
|
|
110
111
|
'-p',
|
|
111
112
|
'Reply OK.',
|
|
113
|
+
...(options.model ? ['--model', options.model] : []),
|
|
112
114
|
'--output-format',
|
|
113
115
|
'stream-json',
|
|
114
116
|
'--verbose',
|
|
@@ -212,6 +214,7 @@ export async function runUsageProbe(account, options, context = {}) {
|
|
|
212
214
|
...normalizePassiveUsage(safe, now),
|
|
213
215
|
source: 'probe',
|
|
214
216
|
};
|
|
217
|
+
measuredWindows = formatPoolUsageWindows(usage.windows);
|
|
215
218
|
await options.store.finishProbe(account, {
|
|
216
219
|
status: 'ok',
|
|
217
220
|
observedAt: now,
|
|
@@ -245,6 +248,6 @@ export async function runUsageProbe(account, options, context = {}) {
|
|
|
245
248
|
const description = context.requestId
|
|
246
249
|
? `usage probe request=${context.requestId}`
|
|
247
250
|
: `local probe account=${poolLabel(account)}`;
|
|
248
|
-
poolInfo({ info: options.log }, `${description} result=${result}${result === 'cancelled' ? ' reason=account selected' : ''}`);
|
|
251
|
+
poolInfo({ info: options.log }, `${description} result=${result}${result === 'ok' && !context.requestId ? ` windows=${measuredWindows}` : ''}${result === 'cancelled' ? ' reason=account selected' : ''}`);
|
|
249
252
|
}
|
|
250
253
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamvibe/poller",
|
|
3
|
-
"version": "0.8.6-rc.
|
|
3
|
+
"version": "0.8.6-rc.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@teamvibe/poller",
|
|
9
|
-
"version": "0.8.6-rc.
|
|
9
|
+
"version": "0.8.6-rc.3",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@aws-sdk/client-dynamodb": "3.1108.0",
|
|
12
12
|
"@aws-sdk/client-sqs": "3.1108.0",
|