@teamvibe/poller 0.8.4-rc.2 → 0.8.4

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 CHANGED
@@ -74,23 +74,23 @@ All configuration is via environment variables (or `~/.teamvibe/.env`):
74
74
  |----------|----------|---------|-------------|
75
75
  | `TEAMVIBE_POLLER_TOKEN` | Yes | — | Your poller authentication token |
76
76
  | `CLAUDE_CODE_OAUTH_TOKEN` | No | — | Claude Code OAuth token (from `claude setup-token`) |
77
- | `EXPOSE_CLAUDE_TOKEN_TO_AGENT` | No | `false` | Opt in per poller to exposing the session token to agent subprocesses; accepts `1`/`true`/`yes`/`on` (case-insensitive) |
78
- | `TEAMVIBE_CLAUDE_OAUTH_TOKEN` | Injected | Unset | In the poller-injected env: with opt-in, the selected pool account's token, otherwise the poller's non-empty `CLAUDE_CODE_OAUTH_TOKEN`; inherited aliases are cleared. Claude settings can override this |
79
- | `TEAMVIBE_POOL_ACCOUNT_ID` | Injected | Unset | In the poller-injected env: with opt-in and a selected pool account, its account ID (no secret); absent without a pool account. Claude settings can override this |
77
+ | `EXPOSE_CLAUDE_TOKEN_TO_AGENT` | No | `true` | Expose the session token to agent subprocesses by default; only `0`/`false`/`no`/`off` (case-insensitive) opt out. Unset or any other value is on |
78
+ | `TEAMVIBE_CLAUDE_OAUTH_TOKEN` | Injected | Session token | In the poller-injected env: the selected pool account's token or the poller's non-empty `CLAUDE_CODE_OAUTH_TOKEN`; unset on explicit opt-out or without a token. Inherited aliases are cleared. Claude settings can override this |
79
+ | `TEAMVIBE_POOL_ACCOUNT_ID` | Injected | Selected account ID | In the poller-injected env: the selected pool account ID (no secret); unset on explicit opt-out or without a pool account. Claude settings can override this |
80
80
  | `TEAMVIBE_API_URL` | No | `https://poller.api.teamvibe.ai` | API endpoint |
81
81
  | `MAX_CONCURRENT_SESSIONS` | No | `5` | Max parallel Claude sessions |
82
82
  | `CLAUDE_CLI_PATH` | No | `claude` | Path to Claude Code CLI |
83
83
  | `TEAMVIBE_DATA_DIR` | No | `~/.teamvibe` | Data directory for brains and state |
84
84
 
85
- Set `EXPOSE_CLAUDE_TOKEN_TO_AGENT=true` in the poller's environment to let agents run nested Claude sessions on the current session's account. Claude CLI strips `CLAUDE_CODE_OAUTH_TOKEN` from Bash subprocess environments by name, so agents can restore it from the alias:
85
+ Agents can run nested Claude sessions on the current session's account by default, for both pool and non-pool sessions. Set `EXPOSE_CLAUDE_TOKEN_TO_AGENT=false` in the poller's environment to opt out (`0`, `no`, and `off` also disable it, case-insensitively); unset or any other value keeps it on. Claude CLI strips `CLAUDE_CODE_OAUTH_TOKEN` from Bash subprocess environments by name, so agents can restore it from the alias:
86
86
 
87
87
  ```bash
88
88
  CLAUDE_CODE_OAUTH_TOKEN="$TEAMVIBE_CLAUDE_OAUTH_TOKEN" claude -p "…"
89
89
  ```
90
90
 
91
- In the environment the poller injects, the alias and account ID follow the selected account on resume and failover; without opt-in, the poller leaves both variables unset. **This guarantee applies only to the injected process environment.** Claude CLI subsequently applies settings-file `env` entries: a brain's `CLAUDE_CONFIG_DIR/settings.json` can restore either variable when the flag is off or override it with a stale value when the flag is on. Project `.claude/settings.json` and `.claude/settings.local.json` can also override them. `settings.local.json` is a project source, not a user file next to `CLAUDE_CONFIG_DIR/settings.json`; recent CLI versions also read project-local settings from the main checkout for worktree sessions ([Claude settings sources](https://code.claude.com/docs/en/settings)). Before each spawn, the poller emits one warning listing the keys and files defining either variable in these settings sources, never their values. It never edits brain settings. Remove conflicting entries yourself to preserve the injected values in the agent's environment.
91
+ In the environment the poller injects, the alias and account ID follow the selected account on resume and failover; with explicit opt-out, the poller leaves both variables unset. **This guarantee applies only to the injected process environment.** Claude CLI subsequently applies settings-file `env` entries: a brain's `CLAUDE_CONFIG_DIR/settings.json` can restore either variable when the flag is off or override it with a stale value when the flag is on. Project `.claude/settings.json` and `.claude/settings.local.json` can also override them. `settings.local.json` is a project source, not a user file next to `CLAUDE_CONFIG_DIR/settings.json`; recent CLI versions also read project-local settings from the main checkout for worktree sessions ([Claude settings sources](https://code.claude.com/docs/en/settings)). Before each spawn, the poller emits one warning listing the keys and files defining either variable in these settings sources, never their values. It never edits brain settings. Remove conflicting entries yourself to preserve the injected values in the agent's environment.
92
92
 
93
- **Security trade-off:** with opt-in, the agent and its subprocesses can read and use the token, bypassing Claude CLI's credential filtering. Enable this only for agents you trust with that credential. With no settings override, nested sessions use the selected session account; this option adds no nested-session usage accounting. Poller-side Claude output logs, returned output/errors, typing status and fallback Slack replies redact the actual injected Claude token and alias values, plus assignments to either token variable name, whether or not opt-in is enabled. This does not prevent agents from using credentials or sending them directly through their own tools.
93
+ **Security trade-off:** by default, the agent and its subprocesses can read and use the token, bypassing Claude CLI's credential filtering. Opt out for agents you do not trust with that credential. With no settings override, nested sessions use the selected session account; this option adds no nested-session usage accounting. Poller-side Claude output logs, returned output/errors, typing status and fallback Slack replies redact the actual injected Claude token and alias values, plus assignments to either token variable name, whether exposure is enabled or explicitly disabled. This does not prevent agents from using credentials or sending them directly through their own tools.
94
94
 
95
95
  ## How It Works
96
96
 
@@ -9,7 +9,7 @@ export interface ClaudeRateLimitWindow {
9
9
  export interface ClaudeRateLimitInfo extends ClaudeRateLimitWindow {
10
10
  status?: string;
11
11
  rateLimitType?: string;
12
- unifiedWindows?: Partial<Record<ClaudeQuotaWindow, ClaudeRateLimitWindow>>;
12
+ unifiedWindows?: Partial<Record<string, ClaudeRateLimitWindow>>;
13
13
  overageStatus?: string;
14
14
  overageDisabledReason?: string;
15
15
  isUsingOverage?: boolean;
@@ -32,7 +32,9 @@ export function parseClaudeRateLimitInfo(value) {
32
32
  const windows = record(obj['unifiedWindows']);
33
33
  if (windows) {
34
34
  const parsed = {};
35
- for (const key of ['five_hour', 'seven_day']) {
35
+ for (const key of Object.keys(windows).slice(0, 128)) {
36
+ if (key.length > 128 || !/^(five_hour|seven_day)(?:_[a-z_]+)?$/.test(key))
37
+ continue;
36
38
  const data = record(windows[key]);
37
39
  if (data)
38
40
  parsed[key] = windowInfo(data);
@@ -460,7 +460,7 @@ export function sweepProcessGroup(proc, graceMs, options = {}) {
460
460
  * would look like if left uncapped.
461
461
  */
462
462
  export function buildSpawnEnv(params) {
463
- const exposeToken = /^(1|true|yes|on)$/i.test(params.processEnv['EXPOSE_CLAUDE_TOKEN_TO_AGENT'] ?? '');
463
+ const exposeToken = !/^(0|false|no|off)$/i.test(params.processEnv['EXPOSE_CLAUDE_TOKEN_TO_AGENT'] ?? '');
464
464
  const accountToken = params.subscriptionAccount?.credential.unwrap();
465
465
  const sessionToken = accountToken ?? params.processEnv['CLAUDE_CODE_OAUTH_TOKEN'];
466
466
  return {
@@ -513,7 +513,7 @@ export function buildSpawnEnv(params) {
513
513
  // watched after the session (and worktree) is gone. See tv#265.
514
514
  POLLER_BRAIN_PATH: params.brainPath,
515
515
  // Claude strips its own token name from Bash subprocesses. This alias
516
- // deliberately opts out of that protection, only at the poller level.
516
+ // exposes the session token by default; the poller can explicitly opt out.
517
517
  // Explicit undefined values also clear inherited/overlay aliases.
518
518
  TEAMVIBE_CLAUDE_OAUTH_TOKEN: exposeToken ? sessionToken || undefined : undefined,
519
519
  TEAMVIBE_POOL_ACCOUNT_ID: exposeToken ? params.subscriptionAccount?.accountId : undefined,
@@ -31,10 +31,11 @@ export declare function poolRunTelemetry(attempts: readonly PoolAttempt[], quota
31
31
  score?: number | undefined;
32
32
  reason?: "headroom_per_hour" | undefined;
33
33
  rateWindow?: {
34
- window: "five_hour" | "seven_day";
35
34
  scope: "opus" | "haiku" | "global" | "sonnet" | "fable" | "uncertain";
36
35
  percent: number | undefined;
37
36
  resetAt: number | undefined;
37
+ kind?: "seven_day_overage_included" | "legacy-uncertain-seven-day" | undefined;
38
+ window: "five_hour" | "seven_day";
38
39
  } | undefined;
39
40
  accountId: string;
40
41
  accountLabel: string;
@@ -48,10 +49,11 @@ export declare function poolRunTelemetry(attempts: readonly PoolAttempt[], quota
48
49
  score?: number | undefined;
49
50
  reason?: "headroom_per_hour" | undefined;
50
51
  rateWindow?: {
51
- window: "five_hour" | "seven_day";
52
52
  scope: "opus" | "haiku" | "global" | "sonnet" | "fable" | "uncertain";
53
53
  percent: number | undefined;
54
54
  resetAt: number | undefined;
55
+ kind?: "seven_day_overage_included" | "legacy-uncertain-seven-day" | undefined;
56
+ window: "five_hour" | "seven_day";
55
57
  } | undefined;
56
58
  accountId?: string | undefined;
57
59
  accountLabel?: string | undefined;
@@ -86,6 +86,7 @@ export function poolRunTelemetry(attempts, quotaDeferEpisodes = 0) {
86
86
  reason: a.reason,
87
87
  rateWindow: {
88
88
  window: a.rateWindow.window,
89
+ ...(a.rateWindow.kind ? { kind: a.rateWindow.kind } : {}),
89
90
  scope: a.rateWindow.scope,
90
91
  percent: a.rateWindow.percent,
91
92
  resetAt: a.rateWindow.resetAt,
@@ -0,0 +1,2 @@
1
+ /** Redact credentials in diagnostics without initializing poller authentication. */
2
+ export declare function redactStartupText(text: string, configuredTokens?: readonly string[]): string;
@@ -0,0 +1,15 @@
1
+ /** Redact credentials in diagnostics without initializing poller authentication. */
2
+ export function redactStartupText(text, configuredTokens = []) {
3
+ let safe = text
4
+ .replace(/https?:\/\/[^\s/@]+@/gi, 'https://[REDACTED]@')
5
+ .replace(/\b(?:Authorization\s*:\s*)?Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]')
6
+ .replace(/\bx-access-token\s*:\s*[^\s/@,;]+/gi, 'x-access-token:[REDACTED]')
7
+ // Cover both Claude's token name and the opt-in subprocess alias in
8
+ // shell assignments and JSON env diagnostics, even without a token list.
9
+ .replace(/(\b(?:CLAUDE_CODE_OAUTH_TOKEN|TEAMVIBE_CLAUDE_OAUTH_TOKEN)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s,;]+)/gi, '$1[REDACTED]');
10
+ for (const token of configuredTokens) {
11
+ if (token)
12
+ safe = safe.split(token).join('[REDACTED]');
13
+ }
14
+ return safe;
15
+ }
@@ -7,7 +7,7 @@ export declare class UnknownStartupFailureError extends Error {
7
7
  constructor(attempts: number);
8
8
  }
9
9
  /** The only formatter used for startup retry and terminal failure logs. */
10
- export declare function redactStartupText(text: string, configuredTokens?: readonly string[]): string;
10
+ export { redactStartupText } from './redact-text.js';
11
11
  export declare function startupFailureDetail(error: unknown, operation: string, configuredTokens?: readonly string[]): string;
12
12
  export declare function isPermanentStartupFailure(error: unknown): boolean;
13
13
  /** Returns false when shutdown cancels startup. Permanent errors propagate to the caller. */
@@ -1,3 +1,4 @@
1
+ import { redactStartupText } from './redact-text.js';
1
2
  import { AuthHttpError, StartupConfigurationError } from './auth-provider.js';
2
3
  export class UnknownStartupFailureError extends Error {
3
4
  attempts;
@@ -34,20 +35,7 @@ function isTransient(error) {
34
35
  return false;
35
36
  }
36
37
  /** The only formatter used for startup retry and terminal failure logs. */
37
- export function redactStartupText(text, configuredTokens = []) {
38
- let safe = text
39
- .replace(/https?:\/\/[^\s/@]+@/gi, 'https://[REDACTED]@')
40
- .replace(/\b(?:Authorization\s*:\s*)?Bearer\s+[^\s,;]+/gi, 'Bearer [REDACTED]')
41
- .replace(/\bx-access-token\s*:\s*[^\s/@,;]+/gi, 'x-access-token:[REDACTED]')
42
- // Cover both Claude's token name and the opt-in subprocess alias in
43
- // shell assignments and JSON env diagnostics, even without a token list.
44
- .replace(/(\b(?:CLAUDE_CODE_OAUTH_TOKEN|TEAMVIBE_CLAUDE_OAUTH_TOKEN)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'[^']*'|[^\s,;]+)/gi, '$1[REDACTED]');
45
- for (const token of configuredTokens) {
46
- if (token)
47
- safe = safe.split(token).join('[REDACTED]');
48
- }
49
- return safe;
50
- }
38
+ export { redactStartupText } from './redact-text.js';
51
39
  export function startupFailureDetail(error, operation, configuredTokens = []) {
52
40
  let detail;
53
41
  if (error instanceof AuthHttpError)
@@ -35,6 +35,8 @@ export type PoolFetchResult<A extends AccountIdentity = AccountIdentity> = {
35
35
  export interface UsageWindow {
36
36
  window: 'five_hour' | 'seven_day';
37
37
  scope: 'global' | 'sonnet' | 'opus' | 'haiku' | 'fable' | 'uncertain';
38
+ /** CLI kind, or the server compatibility provenance for a legacy Fable allowance. */
39
+ kind?: 'seven_day_overage_included' | 'legacy-uncertain-seven-day';
38
40
  status?: 'allowed' | 'allowed_warning' | 'rejected';
39
41
  /** Retained rejections keep their own clock when merged into newer measurements. */
40
42
  observedAt?: number;
@@ -56,29 +56,24 @@ function date(value) {
56
56
  const ms = Date.parse(value);
57
57
  return Number.isFinite(ms) && ms > 0 ? ms : undefined;
58
58
  }
59
+ /** Undocumented CLI stream kinds: only explicit model names and the measured Fable alias. */
60
+ function passiveWindowKind(kind) {
61
+ if (kind === 'seven_day_overage_included')
62
+ return { window: 'seven_day', scope: 'fable', kind };
63
+ const legacy = kind.match(/^(five_hour|seven_day)(?:_(sonnet|opus|haiku|fable))?$/);
64
+ return {
65
+ window: legacy?.[1] === 'five_hour' ? 'five_hour' : 'seven_day',
66
+ scope: legacy ? (legacy[2] ?? 'global') : 'uncertain',
67
+ };
68
+ }
59
69
  export function normalizePassiveUsage(value, observedAt) {
60
70
  const info = record(value);
61
71
  const windows = [];
62
- const unified = record(info['unifiedWindows']);
63
- for (const window of ['five_hour', 'seven_day']) {
64
- const raw = unified[window];
65
- if (raw == null)
66
- continue;
67
- const item = record(raw);
68
- windows.push({ window, scope: 'global', percent: percent(item['utilization'], 1), resetAt: epoch(item['resetsAt']) });
69
- }
70
- const type = info['rateLimitType'];
71
- if (typeof type === 'string') {
72
- const legacy = type.match(/^(five_hour|seven_day)(?:_(sonnet|opus|haiku|fable))?$/);
73
- const candidate = {
74
- window: legacy?.[1] === 'five_hour' ? 'five_hour' : 'seven_day',
75
- scope: legacy ? (legacy[2] ?? 'global') : 'uncertain',
76
- percent: percent(info['utilization'], 1), resetAt: epoch(info['resetsAt']),
77
- ...(info['status'] === 'allowed' || info['status'] === 'allowed_warning' || info['status'] === 'rejected'
78
- ? { status: info['status'] } : {}),
79
- };
72
+ const add = (candidate) => {
80
73
  const existing = windows.find(w => w.window === candidate.window && w.scope === candidate.scope);
81
74
  if (existing) {
75
+ if (candidate.kind !== undefined)
76
+ existing.kind = candidate.kind;
82
77
  if (candidate.status !== undefined)
83
78
  existing.status = candidate.status;
84
79
  // Collapse the same quota while retaining conservative conflict evidence.
@@ -90,6 +85,21 @@ export function normalizePassiveUsage(value, observedAt) {
90
85
  else {
91
86
  windows.push(candidate);
92
87
  }
88
+ };
89
+ for (const [kind, raw] of Object.entries(record(info['unifiedWindows'])).slice(0, 128)) {
90
+ if (raw == null)
91
+ continue;
92
+ const item = record(raw);
93
+ add({ ...passiveWindowKind(kind), percent: percent(item['utilization'], 1), resetAt: epoch(item['resetsAt']) });
94
+ }
95
+ const type = info['rateLimitType'];
96
+ if (typeof type === 'string') {
97
+ add({
98
+ ...passiveWindowKind(type),
99
+ percent: percent(info['utilization'], 1), resetAt: epoch(info['resetsAt']),
100
+ ...(info['status'] === 'allowed' || info['status'] === 'allowed_warning' || info['status'] === 'rejected'
101
+ ? { status: info['status'] } : {}),
102
+ });
93
103
  }
94
104
  return { source: 'passive', observedAt, windows };
95
105
  }
@@ -1,3 +1,4 @@
1
+ import { redactStartupText } from './redact-text.js';
1
2
  import { createHash, randomUUID } from 'node:crypto';
2
3
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
4
  import { join, resolve } from 'node:path';
@@ -34,12 +35,20 @@ function validTime(now) {
34
35
  if (typeof now !== 'number' || !Number.isFinite(now) || now <= 0 || now > 253402300799000)
35
36
  throw new SubscriptionUsageStoreError();
36
37
  }
38
+ const loggedUnknownRateLimitKinds = new Set();
39
+ const knownRateLimitKind = /^(?:(five_hour|seven_day)(?:_(sonnet|opus|haiku|fable))?|seven_day_overage_included)$/;
37
40
  export function safeRateLimitInfo(info) {
38
41
  const parsed = parseClaudeRateLimitInfo(info) ?? {};
39
42
  // Never persist arbitrary status strings or incidental stream fields.
40
43
  if (parsed.status !== 'allowed' && parsed.status !== 'allowed_warning' && parsed.status !== 'rejected')
41
44
  delete parsed.status;
42
- if (parsed.rateLimitType && !/^(five_hour|seven_day)(?:_(sonnet|opus|haiku|fable))?$/.test(parsed.rateLimitType))
45
+ for (const kind of [parsed.rateLimitType, ...Object.keys(parsed.unifiedWindows ?? {})]) {
46
+ if (!kind || kind === 'uncertain' || knownRateLimitKind.test(kind) || !/^[a-z_]{1,128}$/.test(kind) || loggedUnknownRateLimitKinds.has(kind))
47
+ continue;
48
+ loggedUnknownRateLimitKinds.add(kind);
49
+ console.warn(redactStartupText(`[Pool] unknown rate limit kind=${kind}`));
50
+ }
51
+ if (parsed.rateLimitType && !knownRateLimitKind.test(parsed.rateLimitType))
43
52
  parsed.rateLimitType = 'uncertain';
44
53
  delete parsed.overageStatus;
45
54
  delete parsed.overageDisabledReason;
@@ -71,7 +80,7 @@ function safeUsage(value, source) {
71
80
  if (observedAt !== undefined)
72
81
  validTime(observedAt);
73
82
  const source = w['source'] === 'passive' || w['source'] === 'probe' ? w['source'] : undefined;
74
- return { window, scope, percent, resetAt, ...(status ? { status } : {}), ...(observedAt ? { observedAt } : {}), ...(source ? { source } : {}) };
83
+ return { window, scope, ...(w['kind'] === 'seven_day_overage_included' ? { kind: w['kind'] } : {}), percent, resetAt, ...(status ? { status } : {}), ...(observedAt ? { observedAt } : {}), ...(source ? { source } : {}) };
75
84
  });
76
85
  if (usage['windows'].length > 133)
77
86
  windows.push({ window: 'seven_day', scope: 'uncertain' });
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@teamvibe/poller",
3
- "version": "0.8.4-rc.2",
3
+ "version": "0.8.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@teamvibe/poller",
9
- "version": "0.8.4-rc.2",
9
+ "version": "0.8.4",
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.4-rc.2",
3
+ "version": "0.8.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {