@teamvibe/poller 0.8.7 → 0.8.9-rc.1

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.
Files changed (38) hide show
  1. package/README.md +5 -0
  2. package/bin/tv-git-credential.mjs +11 -6
  3. package/dist/brain-manager.js +4 -4
  4. package/dist/claude-spawner.d.ts +4 -2
  5. package/dist/claude-spawner.js +50 -16
  6. package/dist/config.d.ts +4 -0
  7. package/dist/config.js +1 -0
  8. package/dist/connections-client.d.ts +16 -4
  9. package/dist/connections-client.js +42 -11
  10. package/dist/end-of-turn-sentinel.d.ts +2 -0
  11. package/dist/end-of-turn-sentinel.js +2 -0
  12. package/dist/git-credential-bindings.d.ts +2 -0
  13. package/dist/git-credential-bindings.js +4 -0
  14. package/dist/git-credential-env.d.ts +2 -2
  15. package/dist/git-credential-env.js +22 -9
  16. package/dist/git-credential-session.d.ts +6 -6
  17. package/dist/git-credential-session.js +24 -4
  18. package/dist/inbox-watcher.d.ts +2 -2
  19. package/dist/inbox-watcher.js +20 -13
  20. package/dist/poller.d.ts +4 -3
  21. package/dist/poller.js +63 -11
  22. package/dist/redact-text.js +1 -1
  23. package/dist/slack-client.d.ts +3 -1
  24. package/dist/slack-client.js +84 -12
  25. package/dist/subscription-runtime.d.ts +2 -0
  26. package/dist/subscription-runtime.js +6 -2
  27. package/dist/subscription-selection.d.ts +21 -2
  28. package/dist/subscription-selection.js +91 -14
  29. package/dist/subscription-usage-store.d.ts +6 -1
  30. package/dist/subscription-usage-store.js +246 -13
  31. package/dist/thread-routing.d.ts +7 -0
  32. package/dist/thread-routing.js +7 -0
  33. package/dist/types.d.ts +2 -0
  34. package/dist/usage-limit-messages.d.ts +3 -0
  35. package/dist/usage-limit-messages.js +22 -0
  36. package/dist/usage-probe-job.js +31 -3
  37. package/npm-shrinkwrap.json +2 -2
  38. package/package.json +1 -1
@@ -1,6 +1,17 @@
1
1
  import { USAGE_THRESHOLD_PERCENT } from './subscription-selection.js';
2
2
  /** Owner-facing account policy location. */
3
3
  export const FULL_QUOTA_SETTINGS_PATH = 'Connections (account menu → Use until exhausted)';
4
+ /** Quota notices include a calendar date and zone across multi-day holds. */
5
+ function formatQuotaTime(at, timeZone) {
6
+ try {
7
+ const parts = new Intl.DateTimeFormat('en-GB', { timeZone: timeZone || 'UTC', weekday: 'short', day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23', timeZoneName: 'short' }).formatToParts(at);
8
+ const part = (type) => parts.find(p => p.type === type)?.value;
9
+ return `${part('weekday')} ${part('day')} ${part('month')} ${part('hour')}:${part('minute')} ${part('timeZoneName')}`;
10
+ }
11
+ catch {
12
+ return formatQuotaTime(at, 'UTC');
13
+ }
14
+ }
4
15
  export function formatUsageReset(input) {
5
16
  if (input.resetAt === undefined || !Number.isFinite(input.resetAt))
6
17
  return undefined;
@@ -43,6 +54,17 @@ export function formatPoolUsageLimitMessage(input) {
43
54
  const labels = headroom.filter(a => a.label && a.remaining > 0).map(a => `${a.label} ${Number(a.remaining.toFixed(2))} %`).join(', ');
44
55
  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}.`;
45
56
  }
57
+ if (input.blockedWindows?.length) {
58
+ const windows = [...new Map(input.blockedWindows.map(w => [`${w.window}:${w.scope}`, w])).values()];
59
+ const names = windows.map(w => `${w.scope === 'global' ? 'global' : w.scope === 'uncertain' ? 'Claude' : w.scope[0].toUpperCase() + w.scope.slice(1)} ${w.window === 'seven_day' ? 'weekly' : 'five-hour'} quota`);
60
+ const global = windows.some(w => w.scope === 'global' || w.scope === 'uncertain');
61
+ const resets = input.resetAt !== undefined ? `resets ${formatQuotaTime(input.resetAt, input.timeZone)}` : 'reset time unavailable';
62
+ const retry = input.eligibleAt !== undefined ? `I'll retry automatically at ${formatQuotaTime(input.eligibleAt, input.timeZone)}` : "I'll retry automatically after the next reset";
63
+ const blocked = windows.length === 1 && !global
64
+ ? `${names[0]} is exhausted on every account in this workspace (${resets}).`
65
+ : `All Claude accounts in this workspace have hit their usage limit (${names.join(', ')}; ${resets}).`;
66
+ return `⏳ ${blocked} Your message is saved. ${retry}${global ? '.' : '; other models keep working.'}`;
67
+ }
46
68
  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'}.`;
47
69
  }
48
70
  export function formatLegacyUsageLimitMessage(input) {
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { mkdtemp, rm, mkdir } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
- import { selectSubscriptionAccount, normalizePassiveUsage } from './subscription-selection.js';
5
+ import { selectSubscriptionAccount, normalizePassiveUsage, reactiveUsageWindow } from './subscription-selection.js';
6
6
  import { safeRateLimitInfo, usageStateForSelection, } from './subscription-usage-store.js';
7
7
  import { parseClaudeRateLimitInfo, } from './claude-rate-limit.js';
8
8
  import { formatPoolUsageWindows, poolInfo, poolLabel } from './pool-logging.js';
@@ -210,18 +210,46 @@ export async function runUsageProbe(account, options, context = {}) {
210
210
  return result;
211
211
  const now = Date.now();
212
212
  const safe = safeRateLimitInfo(info);
213
+ const normalized = normalizePassiveUsage(safe, now);
213
214
  const usage = {
214
- ...normalizePassiveUsage(safe, now),
215
+ ...normalized,
215
216
  source: 'probe',
217
+ windows: normalized.windows.map(w => ({ ...w,
218
+ ...(w.status === undefined && w.percent !== undefined && (safe.status === 'allowed' || safe.status === 'allowed_warning') ? { status: safe.status } : {}),
219
+ })),
216
220
  };
217
221
  measuredWindows = formatPoolUsageWindows(usage.windows);
222
+ const before = usageStateForSelection(await options.store.read(account), now);
218
223
  await options.store.finishProbe(account, {
219
224
  status: 'ok',
220
225
  observedAt: now,
221
226
  usage,
222
227
  });
223
228
  const stored = await options.store.read(account);
224
- if (selectSubscriptionAccount({ pool: { kind: 'ready', accounts: [account] }, states: { [account.accountId]: usageStateForSelection(stored, now) }, model: undefined, now }).kind === 'selected') {
229
+ const after = usageStateForSelection(stored, now);
230
+ // Keep the combined comparison, and compare each rejected scope/window
231
+ // independently so an unrelated, later hold cannot hide an improvement.
232
+ const pairs = new Map();
233
+ for (const state of [before, after])
234
+ for (const w of [...(state.usage?.windows ?? []), ...(state.reactive ?? []).map(reactiveUsageWindow)]) {
235
+ if (w.status === 'rejected')
236
+ pairs.set(`${w.scope}.${w.window}`, w);
237
+ }
238
+ const eligibility = (state, pair) => {
239
+ const matches = (w) => !pair || w.scope === pair.scope && w.window === pair.window;
240
+ const scoped = pair ? {
241
+ usage: state.usage ? { ...state.usage, windows: state.usage.windows.filter(matches) } : undefined,
242
+ reactive: state.reactive?.filter(r => matches(reactiveUsageWindow(r))),
243
+ } : state;
244
+ return selectSubscriptionAccount({ pool: { kind: 'ready', accounts: [account] }, states: { [account.accountId]: scoped }, model: pair?.scope, now });
245
+ };
246
+ const improved = [undefined, ...pairs.values()].some(pair => {
247
+ const previous = eligibility(before, pair);
248
+ const current = eligibility(after, pair);
249
+ return current.kind === 'selected' && (!pair || previous.kind === 'deferred') ||
250
+ current.kind === 'deferred' && previous.kind === 'deferred' && current.eligibleAt < previous.eligibleAt;
251
+ });
252
+ if (improved) {
225
253
  try {
226
254
  options.onHeadroom?.(account.workspaceId);
227
255
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@teamvibe/poller",
3
- "version": "0.8.7",
3
+ "version": "0.8.9-rc.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@teamvibe/poller",
9
- "version": "0.8.7",
9
+ "version": "0.8.9-rc.1",
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.7",
3
+ "version": "0.8.9-rc.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {