@teamvibe/poller 0.8.6-rc.2 → 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 +12 -0
- package/dist/pool-logging.js +25 -12
- 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 +5 -2
- package/dist/subscription-selection.d.ts +1 -0
- package/dist/subscription-selection.js +2 -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
|
@@ -14,6 +14,18 @@ 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;
|
|
18
30
|
export declare function formatPoolUsageWindows(windows: readonly UsageWindow[], includeScope?: boolean): string;
|
|
19
31
|
/** Log the real store promise without changing its rejection or retry behavior. */
|
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
|
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;
|
|
@@ -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, {
|
|
@@ -88,6 +88,7 @@ export type SelectionOutcome<A extends AccountIdentity = AccountIdentity> = ({
|
|
|
88
88
|
export declare function record(value: unknown): Record<string, unknown>;
|
|
89
89
|
export declare function normalizePassiveUsage(value: unknown, observedAt: number): UsageObservation;
|
|
90
90
|
export declare function normalizeProbeUsage(value: unknown, observedAt: number): UsageObservation;
|
|
91
|
+
export declare function family(model: string | undefined): 'sonnet' | 'opus' | 'haiku' | 'fable' | undefined;
|
|
91
92
|
export declare function reactiveEligibleAt(reactive: ReactiveExhaustion): number;
|
|
92
93
|
/** Pure policy. Unknown is eligible after every known/reactive blocker is ruled out. */
|
|
93
94
|
export declare function selectSubscriptionAccount<A extends AccountIdentity>(input: {
|
|
@@ -143,7 +143,7 @@ export function normalizeProbeUsage(value, observedAt) {
|
|
|
143
143
|
}
|
|
144
144
|
return { source: 'probe', observedAt, windows };
|
|
145
145
|
}
|
|
146
|
-
function family(model) {
|
|
146
|
+
export function family(model) {
|
|
147
147
|
return model?.toLowerCase().match(/^(?:claude-)?(sonnet|opus|haiku|fable)(?:-\d[\w.-]*)?(?:\[1m\])?$/)?.[1];
|
|
148
148
|
}
|
|
149
149
|
function applicable(scope, model) {
|
|
@@ -209,7 +209,7 @@ export function selectSubscriptionAccount(input) {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
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)) : [];
|
|
212
|
-
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)));
|
|
213
213
|
for (const w of windows) {
|
|
214
214
|
if (w.percent !== undefined && w.percent >= (account.useFullQuota ? 100 : USAGE_THRESHOLD_PERCENT))
|
|
215
215
|
blockers.push(usageWindowExpiresAt(usage, w));
|
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",
|