@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.
@@ -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 ? { resolved, modelNormalizedFrom, claudeConfigDir, account: prepared?.account, shouldStop: poolRuntime.shouldStop, resumePending: fetchedPool?.kind !== 'no_accounts_configured' && poolRuntime.pin?.resumePending === true } : undefined;
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;
@@ -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;
@@ -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
- export function logPoolSelection(log, pool, states, model, now, selected, logAllExhausted = true) {
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
- for (const account of pool.accounts) {
49
- if (!account.enabled) {
50
- poolInfo(log, `skipped account=${poolLabel(account)} reason=disabled`);
51
- continue;
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
- poolInfo(log, `skipped account=${poolLabel(account)} reason=${usageOnly.kind === 'deferred' ? `usage >=${account.useFullQuota ? 100 : 90}%` : 'reactive quota'} eligibleAt=${new Date(candidate.eligibleAt).toISOString()}`);
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.map(w => `${w.window}${source === 'passive' ? `/${w.scope}` : ''}:${w.percent === undefined ? 'unknown' : `${Number(w.percent.toFixed(2))}%`}`).join(',') : '';
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}`);
@@ -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;
@@ -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 (log && pool.kind === 'ready' && currentAccountId && currentAccountId !== selected.account.accountId) {
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
- poolInfo(log, `switch from=${poolLabel(previous ?? {})} to=${poolLabel(selected.account)} reason=${previousSelection?.kind === 'selected' ? 'better headroom' : 'pinned account unavailable'}`);
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 reset; probes retain their short TTL. */
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 reset; probes retain their short TTL. */
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 usage.source === 'passive' ? window.resetAt ?? ttlExpiry : Math.min(window.resetAt ?? ttlExpiry, ttlExpiry);
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;
@@ -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
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@teamvibe/poller",
3
- "version": "0.8.6-rc.1",
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.1",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamvibe/poller",
3
- "version": "0.8.6-rc.1",
3
+ "version": "0.8.6-rc.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {