agent-relay-runner 0.77.1 → 0.78.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
- "version": "0.77.1",
3
+ "version": "0.78.1",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "directory": "runner"
21
21
  },
22
22
  "dependencies": {
23
- "agent-relay-sdk": "0.2.52"
23
+ "agent-relay-sdk": "0.2.54"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/bun": "latest",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.77.1",
4
+ "version": "0.78.1",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
package/src/quota.ts CHANGED
@@ -1,606 +1,31 @@
1
- import { readFile } from "node:fs/promises";
2
- import { createHash } from "node:crypto";
3
- import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
4
- import { homedir, hostname } from "node:os";
5
- import { dirname, join } from "node:path";
6
- import type { ContextProbeMetrics, ProviderQuotaError, ProviderQuotaUpdateInput, QuotaState, QuotaWindowName } from "agent-relay-sdk";
7
- import { errMessage, isRecord, quotaStateFromProbeMetrics, stringValue } from "agent-relay-sdk";
8
- import { sanitizeFsName } from "agent-relay-sdk/fs-name";
9
- import type { ManagedProcess } from "./adapter";
10
- import { agentRelayHome, runnerOutboxDirWithInfoFallback } from "./config";
11
-
12
- const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
13
- const CLAUDE_OAUTH_BETA = "oauth-2025-04-20";
14
- const QUOTA_POLL_INTERVAL_MS = 5 * 60 * 1000;
15
- const QUOTA_FAST_RETRY_MS = 15 * 1000;
16
- const QUOTA_FAILURE_LOG_INTERVAL_MS = 5 * 60 * 1000;
17
- const QUOTA_LOCK_STALE_MS = 60 * 1000;
18
-
19
- type CodexQuotaClient = {
20
- isConnected?: () => boolean;
21
- accountRateLimitsRead?: () => Promise<unknown>;
22
- };
23
-
24
- type QuotaIdentity = {
25
- provider: string;
26
- accountKey: string;
27
- credentialsPath?: string;
28
- };
29
-
30
- type RunnerQuotaSample = {
31
- quota?: QuotaState;
32
- accountKey?: string;
33
- };
34
-
35
- type SharedQuotaEntry = {
36
- provider: string;
37
- accountKey: string;
38
- lastAttemptAt?: number;
39
- nextAttemptAt?: number;
40
- quota?: QuotaState;
41
- lastError?: ProviderQuotaError | null;
42
- };
43
-
44
- export class RunnerQuotaPoller {
45
- private quotaState?: QuotaState;
46
- private timer?: Timer;
47
- private inFlight = false;
48
- private active = false;
49
- private activeLockDir?: string;
50
- private lastLog?: { key: string; at: number };
51
-
52
- constructor(private readonly options: {
53
- provider: string;
54
- agentId: string;
55
- getProcess: () => ManagedProcess | undefined;
56
- onUpdate: () => void;
57
- log: (message: string) => void;
58
- reportProviderQuota?: (input: ProviderQuotaUpdateInput) => Promise<unknown>;
59
- quotaStateDir?: string;
60
- fetchImpl?: typeof fetch;
61
- }) {}
62
-
63
- get current(): QuotaState | undefined {
64
- return this.quotaState;
65
- }
66
-
67
- start(): void {
68
- this.stop();
69
- if (!quotaProviderSupported(this.options.provider)) return;
70
- this.active = true;
71
- void this.refresh();
72
- }
73
-
74
- stop(): void {
75
- this.active = false;
76
- if (this.timer) clearTimeout(this.timer);
77
- this.timer = undefined;
78
- const lockDir = this.activeLockDir;
79
- this.activeLockDir = undefined;
80
- if (lockDir) releaseQuotaLock(lockDir);
81
- }
82
-
83
- clear(): void {
84
- this.quotaState = undefined;
85
- }
86
-
87
- private schedule(delayMs = QUOTA_POLL_INTERVAL_MS): void {
88
- if (this.timer) clearTimeout(this.timer);
89
- this.timer = undefined;
90
- if (!this.active || !quotaProviderSupported(this.options.provider)) return;
91
- this.timer = setTimeout(() => {
92
- this.timer = undefined;
93
- void this.refresh();
94
- }, Math.max(1_000, delayMs));
95
- }
96
-
97
- private async refresh(): Promise<void> {
98
- if (this.inFlight) return;
99
- this.inFlight = true;
100
- const now = Date.now();
101
- let identity: QuotaIdentity | undefined;
102
- let lockDir: string | undefined;
103
- try {
104
- const process = this.options.getProcess();
105
- identity = await resolveRunnerQuotaIdentity({
106
- provider: this.options.provider,
107
- process,
108
- });
109
- const attempt = beginSharedQuotaAttempt(identity, this.options.quotaStateDir, now);
110
- if (!attempt.poll) {
111
- if (attempt.entry?.quota && this.active) {
112
- this.quotaState = attempt.entry.quota;
113
- this.options.onUpdate();
114
- }
115
- this.schedule(attempt.retryInMs);
116
- return;
117
- }
118
- lockDir = attempt.lockDir;
119
- this.activeLockDir = lockDir;
120
- const sample = await collectRunnerQuotaSample({
121
- provider: this.options.provider,
122
- agentId: this.options.agentId,
123
- process,
124
- credentialsPath: identity.credentialsPath,
125
- fetchImpl: this.options.fetchImpl,
126
- });
127
- const quota = sample.quota;
128
- if (!quota) throw new QuotaCollectionError("creds_not_ready", `${this.options.provider} quota source is not ready`);
129
- if (this.active) {
130
- const update: ProviderQuotaUpdateInput = {
131
- provider: identity.provider,
132
- accountKey: sample.accountKey ?? identity.accountKey,
133
- quota,
134
- lastAttemptAt: quota.updatedAt,
135
- sourceAgentId: this.options.agentId,
136
- };
137
- await this.reportProviderQuota(update);
138
- finishSharedQuotaAttempt(identity, {
139
- provider: update.provider,
140
- accountKey: update.accountKey,
141
- quota,
142
- lastAttemptAt: update.lastAttemptAt,
143
- nextAttemptAt: now + QUOTA_POLL_INTERVAL_MS,
144
- lastError: null,
145
- }, this.options.quotaStateDir);
146
- this.quotaState = quota;
147
- this.options.onUpdate();
148
- }
149
- this.schedule();
150
- } catch (error) {
151
- const retryAfterMs = quotaRetryAfterMs(error);
152
- const lastError = providerQuotaErrorFromCollectorError(error, retryAfterMs);
153
- const lastAttemptAt = Date.now();
154
- const retryDelayMs = retryAfterMs ?? (identity && !readSharedQuotaEntry(identity, this.options.quotaStateDir)?.lastAttemptAt ? QUOTA_FAST_RETRY_MS : QUOTA_POLL_INTERVAL_MS);
155
- if (identity && this.active) {
156
- await this.reportProviderQuota({
157
- provider: identity.provider,
158
- accountKey: identity.accountKey,
159
- lastAttemptAt,
160
- lastError,
161
- sourceAgentId: this.options.agentId,
162
- }).catch((publishError) => {
163
- this.options.log(`quota status publish failed: ${errMessage(publishError)}`);
164
- });
165
- finishSharedQuotaAttempt(identity, {
166
- provider: identity.provider,
167
- accountKey: identity.accountKey,
168
- lastAttemptAt,
169
- nextAttemptAt: lastAttemptAt + retryDelayMs,
170
- lastError,
171
- }, this.options.quotaStateDir);
172
- }
173
- if (this.active) this.logFailure(error, retryAfterMs);
174
- this.schedule(retryDelayMs);
175
- } finally {
176
- if (lockDir && this.activeLockDir === lockDir) {
177
- releaseQuotaLock(lockDir);
178
- this.activeLockDir = undefined;
179
- }
180
- this.inFlight = false;
181
- }
182
- }
183
-
184
- private async reportProviderQuota(input: ProviderQuotaUpdateInput): Promise<void> {
185
- if (!this.options.reportProviderQuota) return;
186
- await this.options.reportProviderQuota(input);
187
- }
188
-
189
- private logFailure(error: unknown, retryAfterMs: number | undefined): void {
190
- const key = retryAfterMs !== undefined ? `retry-after:${retryAfterMs}` : errMessage(error);
191
- const now = Date.now();
192
- if (this.lastLog?.key === key && now - this.lastLog.at < QUOTA_FAILURE_LOG_INTERVAL_MS) return;
193
- this.lastLog = { key, at: now };
194
- const suffix = retryAfterMs !== undefined ? `; retrying in ${Math.round(retryAfterMs / 1000)}s` : "";
195
- this.options.log(`quota refresh failed${suffix}: ${errMessage(error)}`);
196
- }
197
- }
198
-
199
- export class QuotaRetryAfterError extends Error {
200
- constructor(readonly retryAfterMs: number, message = "quota source requested retry backoff") {
201
- super(message);
202
- this.name = "QuotaRetryAfterError";
203
- }
204
- }
205
-
206
- class QuotaCollectionError extends Error {
207
- constructor(readonly type: ProviderQuotaError["type"], message: string) {
208
- super(message);
209
- this.name = "QuotaCollectionError";
210
- }
211
- }
212
-
213
- function quotaRetryAfterMs(error: unknown): number | undefined {
214
- return error instanceof QuotaRetryAfterError ? error.retryAfterMs : undefined;
215
- }
216
-
217
- function providerQuotaErrorFromCollectorError(error: unknown, retryAfterMs: number | undefined): ProviderQuotaError {
218
- if (retryAfterMs !== undefined) {
219
- return { type: "rate_limited", message: errMessage(error), retryAfterMs };
220
- }
221
- if (error instanceof QuotaCollectionError) {
222
- return { type: error.type, message: error.message };
223
- }
224
- return { type: "other", message: errMessage(error) };
225
- }
226
-
227
- export async function resolveRunnerQuotaIdentity(input: {
228
- provider: string;
229
- process?: ManagedProcess;
230
- }): Promise<QuotaIdentity> {
231
- const provider = input.provider.trim().toLowerCase();
232
- if (provider === "claude") {
233
- const credentialsPath = claudeCredentialsPathFromProcess(input.process);
234
- return {
235
- provider,
236
- accountKey: hostProviderAccountKey(),
237
- credentialsPath,
238
- };
239
- }
240
- return {
241
- provider,
242
- accountKey: codexAccountKeyFromProcess(input.process) ?? hostProviderAccountKey(),
243
- };
244
- }
245
-
246
- async function collectRunnerQuotaSample(input: {
247
- provider: string;
248
- agentId: string;
249
- process?: ManagedProcess;
250
- credentialsPath?: string;
251
- fetchImpl?: typeof fetch;
252
- }): Promise<RunnerQuotaSample> {
253
- if (input.provider === "claude") return collectClaudeQuotaSample({ agentId: input.agentId, credentialsPath: input.credentialsPath, fetchImpl: input.fetchImpl });
254
- if (input.provider !== "codex") return {};
255
- const client = input.process?.meta?.client as CodexQuotaClient | undefined;
256
- if (!client?.isConnected?.() || typeof client.accountRateLimitsRead !== "function") return {};
257
- return collectCodexQuotaSample({
258
- agentId: codexProcessAgentId(input.process, input.agentId),
259
- rateLimitsRead: () => client.accountRateLimitsRead!(),
260
- });
261
- }
262
-
263
- async function collectClaudeQuotaSample(options: {
264
- agentId: string;
265
- credentialsPath?: string;
266
- fetchImpl?: typeof fetch;
267
- now?: number;
268
- }): Promise<RunnerQuotaSample> {
269
- const payload = await fetchClaudeUsagePayload(options);
270
- if (payload === undefined) return {};
271
- return {
272
- quota: claudeUsageQuotaState(payload, options.agentId, options.now ?? Date.now()),
273
- accountKey: providerPayloadAccountKey(payload),
274
- };
275
- }
276
-
277
- export async function collectClaudeQuotaState(options: {
278
- agentId: string;
279
- credentialsPath?: string;
280
- fetchImpl?: typeof fetch;
281
- now?: number;
282
- }): Promise<QuotaState | undefined> {
283
- return (await collectClaudeQuotaSample(options)).quota;
284
- }
285
-
286
- async function fetchClaudeUsagePayload(options: {
287
- credentialsPath?: string;
288
- fetchImpl?: typeof fetch;
289
- }): Promise<unknown | undefined> {
290
- const accessToken = await readClaudeOAuthAccessToken(options.credentialsPath ?? defaultClaudeCredentialsPath());
291
- if (!accessToken) return undefined;
292
- const fetchImpl = options.fetchImpl ?? fetch;
293
- const response = await fetchImpl(CLAUDE_USAGE_URL, {
294
- method: "GET",
295
- headers: {
296
- authorization: `Bearer ${accessToken}`,
297
- "anthropic-beta": CLAUDE_OAUTH_BETA,
298
- },
299
- signal: AbortSignal.timeout(10_000),
300
- });
301
- if (response.status === 429) {
302
- throw new QuotaRetryAfterError(retryAfterHeaderMs(response.headers.get("retry-after")) ?? 60_000, "Claude quota source returned 429");
303
- }
304
- if (response.status === 401 || response.status === 403) throw new QuotaCollectionError("auth", `Claude quota source returned ${response.status}`);
305
- if (!response.ok) throw new QuotaCollectionError("other", `Claude quota source returned ${response.status}`);
306
- return response.json();
307
- }
308
-
309
- export function claudeUsageQuotaState(payload: unknown, agentId: string, now = Date.now()): QuotaState | undefined {
310
- return quotaStateFromProbeMetrics({
311
- agentId,
312
- contextPercent: 0,
313
- quotaWindows: [
314
- quotaWindowFromAnthropicUsage("five_hour", payload),
315
- quotaWindowFromAnthropicUsage("seven_day", payload),
316
- ].filter((window): window is NonNullable<ContextProbeMetrics["quotaWindows"]>[number] => Boolean(window)),
317
- source: "api",
318
- confidence: "reported",
319
- timestamp: now,
320
- });
321
- }
322
-
323
- export async function collectCodexQuotaState(input: {
324
- agentId: string;
325
- rateLimitsRead: () => Promise<unknown>;
326
- now?: number;
327
- }): Promise<QuotaState | undefined> {
328
- return (await collectCodexQuotaSample(input)).quota;
329
- }
330
-
331
- async function collectCodexQuotaSample(input: {
332
- agentId: string;
333
- rateLimitsRead: () => Promise<unknown>;
334
- now?: number;
335
- }): Promise<RunnerQuotaSample> {
336
- try {
337
- const payload = await input.rateLimitsRead();
338
- return {
339
- quota: codexRateLimitsQuotaState(payload, input.agentId, input.now ?? Date.now()),
340
- accountKey: providerPayloadAccountKey(payload),
341
- };
342
- } catch (error) {
343
- const retryAfter = codexRetryAfterMs(error);
344
- if (retryAfter !== undefined) throw new QuotaRetryAfterError(retryAfter, "Codex quota source returned 429");
345
- throw error;
346
- }
347
- }
348
-
349
- export function codexRateLimitsQuotaState(payload: unknown, agentId: string, now = Date.now()): QuotaState | undefined {
350
- const rateLimits = isRecord(payload) && isRecord(payload.rateLimits) ? payload.rateLimits
351
- : isRecord(payload) && isRecord(payload.rate_limits) ? payload.rate_limits
352
- : payload;
353
- return quotaStateFromProbeMetrics({
354
- agentId,
355
- contextPercent: 0,
356
- quotaWindows: [
357
- quotaWindowFromCodexRateLimit("primary", rateLimits),
358
- quotaWindowFromCodexRateLimit("secondary", rateLimits),
359
- ].filter((window): window is NonNullable<ContextProbeMetrics["quotaWindows"]>[number] => Boolean(window)),
360
- source: "api",
361
- confidence: "reported",
362
- timestamp: now,
363
- });
364
- }
365
-
366
- function defaultClaudeCredentialsPath(): string {
367
- return join(defaultClaudeConfigDir(), ".credentials.json");
368
- }
369
-
370
- export function defaultClaudeConfigDir(): string {
371
- return process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
372
- }
373
-
374
- function claudeCredentialsPathFromProcess(proc: ManagedProcess | undefined): string {
375
- const env = processEnvFromMeta(proc);
376
- const configDir = stringValue(env?.CLAUDE_CONFIG_DIR) ?? process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
377
- return join(configDir, ".credentials.json");
378
- }
379
-
380
- function codexAccountKeyFromProcess(proc: ManagedProcess | undefined): string | undefined {
381
- const env = processEnvFromMeta(proc);
382
- const codexHome = stringValue(env?.CODEX_HOME) ?? process.env.CODEX_HOME;
383
- return codexHome ? `home:${shortHash(codexHome)}` : undefined;
384
- }
385
-
386
- function processEnvFromMeta(proc: ManagedProcess | undefined): Record<string, unknown> | undefined {
387
- const direct = isRecord(proc?.meta?.env) ? proc.meta.env : undefined;
388
- if (direct) return direct;
389
- const config = isRecord(proc?.meta?.config) ? proc.meta.config : undefined;
390
- return isRecord(config?.env) ? config.env : undefined;
391
- }
392
-
393
- function hostProviderAccountKey(): string {
394
- return `host:${hostname()}`;
395
- }
396
-
397
- export function beginSharedQuotaAttempt(identity: QuotaIdentity, stateDir?: string, now = Date.now()): {
398
- poll: boolean;
399
- retryInMs: number;
400
- entry?: SharedQuotaEntry;
401
- lockDir?: string;
402
- } {
403
- const entry = readSharedQuotaEntry(identity, stateDir);
404
- const nextAttemptAt = entry?.nextAttemptAt;
405
- if (nextAttemptAt !== undefined && nextAttemptAt > now) {
406
- return { poll: false, retryInMs: nextAttemptAt - now, entry };
407
- }
408
-
409
- const lockDir = sharedQuotaLockDir(identity, stateDir);
410
- if (!acquireQuotaLock(lockDir, now)) {
411
- return { poll: false, retryInMs: QUOTA_FAST_RETRY_MS, entry };
412
- }
413
- return { poll: true, retryInMs: 0, entry, lockDir };
414
- }
415
-
416
- function finishSharedQuotaAttempt(identity: QuotaIdentity, entry: SharedQuotaEntry, stateDir?: string): void {
417
- const path = sharedQuotaStatePath(identity, stateDir);
418
- mkdirSync(dirname(path), { recursive: true });
419
- writeFileSync(path, JSON.stringify(entry, null, 2));
420
- }
421
-
422
- function readSharedQuotaEntry(identity: QuotaIdentity, stateDir?: string): SharedQuotaEntry | undefined {
423
- try {
424
- const parsed = JSON.parse(readFileSync(sharedQuotaStatePath(identity, stateDir), "utf8")) as unknown;
425
- if (!isRecord(parsed)) return undefined;
426
- const provider = stringValue(parsed.provider);
427
- const accountKey = stringValue(parsed.accountKey);
428
- if (!provider || !accountKey) return undefined;
429
- return {
430
- provider,
431
- accountKey,
432
- ...(typeof parsed.lastAttemptAt === "number" && Number.isFinite(parsed.lastAttemptAt) ? { lastAttemptAt: parsed.lastAttemptAt } : {}),
433
- ...(typeof parsed.nextAttemptAt === "number" && Number.isFinite(parsed.nextAttemptAt) ? { nextAttemptAt: parsed.nextAttemptAt } : {}),
434
- ...(isQuotaState(parsed.quota) ? { quota: parsed.quota } : {}),
435
- ...(parsed.lastError === null || isRecord(parsed.lastError) ? { lastError: parsed.lastError as ProviderQuotaError | null } : {}),
436
- };
437
- } catch {
438
- return undefined;
439
- }
440
- }
441
-
442
- function sharedQuotaStatePath(identity: QuotaIdentity, stateDir?: string): string {
443
- return join(sharedQuotaStateDir(stateDir), `${sharedQuotaKey(identity)}.json`);
444
- }
445
-
446
- function sharedQuotaLockDir(identity: QuotaIdentity, stateDir?: string): string {
447
- return join(sharedQuotaStateDir(stateDir), `${sharedQuotaKey(identity)}.lock`);
448
- }
449
-
450
- function sharedQuotaStateDir(stateDir?: string): string {
451
- return stateDir ?? join(runnerOutboxDirWithInfoFallback() ?? agentRelayHome(), "quota");
452
- }
453
-
454
- function sharedQuotaKey(identity: QuotaIdentity): string {
455
- return sanitizeFsName(`${identity.provider}-${identity.accountKey}`);
456
- }
457
-
458
- function acquireQuotaLock(lockDir: string, now = Date.now()): boolean {
459
- mkdirSync(dirname(lockDir), { recursive: true });
460
- try {
461
- mkdirSync(lockDir, { recursive: false });
462
- writeFileSync(join(lockDir, "owner"), String(now));
463
- return true;
464
- } catch {
465
- const ownerPath = join(lockDir, "owner");
466
- const lockedAt = quotaLockTimestamp(lockDir, ownerPath);
467
- if (lockedAt !== undefined && now - lockedAt <= QUOTA_LOCK_STALE_MS) return false;
468
- rmSync(lockDir, { recursive: true, force: true });
469
- try {
470
- mkdirSync(lockDir, { recursive: false });
471
- writeFileSync(ownerPath, String(now));
472
- return true;
473
- } catch {
474
- return false;
475
- }
476
- }
477
- }
478
-
479
- function releaseQuotaLock(lockDir: string): void {
480
- rmSync(lockDir, { recursive: true, force: true });
481
- }
482
-
483
- function quotaLockTimestamp(lockDir: string, ownerPath: string): number | undefined {
484
- try {
485
- const lockedAt = Number(readFileSync(ownerPath, "utf8"));
486
- if (Number.isFinite(lockedAt)) return lockedAt;
487
- } catch {}
488
- try {
489
- return statSync(lockDir).mtimeMs;
490
- } catch {
491
- return undefined;
492
- }
493
- }
494
-
495
- function isQuotaState(value: unknown): value is QuotaState {
496
- return isRecord(value) && Array.isArray(value.windows) && typeof value.updatedAt === "number";
497
- }
498
-
499
- export function providerPayloadAccountKey(payload: unknown): string | undefined {
500
- if (!isRecord(payload)) return undefined;
501
- const direct = stringValue(payload.accountKey) ??
502
- stringValue(payload.account_key) ??
503
- stringValue(payload.accountId) ??
504
- stringValue(payload.account_id) ??
505
- stringValue(payload.orgId) ??
506
- stringValue(payload.org_id) ??
507
- stringValue(payload.organizationId) ??
508
- stringValue(payload.organization_id);
509
- if (direct) return direct;
510
- const account = isRecord(payload.account) ? payload.account : undefined;
511
- const org = isRecord(payload.org) ? payload.org : isRecord(payload.organization) ? payload.organization : undefined;
512
- return stringValue(account?.id) ?? stringValue(org?.id);
513
- }
514
-
515
- function shortHash(value: string): string {
516
- return createHash("sha256").update(value).digest("hex").slice(0, 16);
517
- }
518
-
519
- function quotaProviderSupported(provider: string): boolean {
520
- return provider === "claude" || provider === "codex";
521
- }
522
-
523
- function codexProcessAgentId(process: ManagedProcess | undefined, fallback: string): string {
524
- const config = isRecord(process?.meta?.config) ? process.meta.config : undefined;
525
- return stringValue(config?.agentId) ?? fallback;
526
- }
527
-
528
- async function readClaudeOAuthAccessToken(path: string): Promise<string | undefined> {
529
- try {
530
- const parsed = JSON.parse(await readFile(path, "utf8")) as unknown;
531
- const oauth = isRecord(parsed) && isRecord(parsed.claudeAiOauth) ? parsed.claudeAiOauth : undefined;
532
- const token = oauth?.accessToken;
533
- return typeof token === "string" && token.trim() ? token : undefined;
534
- } catch {
535
- return undefined;
536
- }
537
- }
538
-
539
- function quotaWindowFromAnthropicUsage(name: QuotaWindowName, payload: unknown): NonNullable<ContextProbeMetrics["quotaWindows"]>[number] | undefined {
540
- if (!isRecord(payload)) return undefined;
541
- const raw = isRecord(payload[name]) ? payload[name] : undefined;
542
- const utilization = normalizePercentLike(raw?.utilization);
543
- if (utilization === undefined) return undefined;
544
- return {
545
- name,
546
- utilization,
547
- ...(resetValueMs(raw?.resets_at) !== undefined ? { resetsAt: resetValueMs(raw?.resets_at)! } : {}),
548
- unit: "%",
549
- };
550
- }
551
-
552
- function quotaWindowFromCodexRateLimit(name: QuotaWindowName, payload: unknown): NonNullable<ContextProbeMetrics["quotaWindows"]>[number] | undefined {
553
- if (!isRecord(payload)) return undefined;
554
- const raw = isRecord(payload[name]) ? payload[name] : undefined;
555
- const utilization = normalizePercentLike(raw?.usedPercent ?? raw?.used_percent ?? raw?.utilization);
556
- if (utilization === undefined) return undefined;
557
- return {
558
- name,
559
- utilization,
560
- ...(resetValueMs(raw?.resetsAt ?? raw?.resets_at) !== undefined ? { resetsAt: resetValueMs(raw?.resetsAt ?? raw?.resets_at)! } : {}),
561
- unit: "%",
562
- };
563
- }
564
-
565
- function normalizePercentLike(value: unknown): number | undefined {
566
- const number = numberValue(value);
567
- if (number === undefined) return undefined;
568
- const fraction = number > 1 ? number / 100 : number;
569
- return Math.max(0, Math.min(1, fraction));
570
- }
571
-
572
- function resetValueMs(value: unknown): number | undefined {
573
- const number = numberValue(value);
574
- if (number !== undefined) return number < 10_000_000_000 ? number * 1000 : number;
575
- if (typeof value !== "string" || !value.trim()) return undefined;
576
- const parsed = Date.parse(value);
577
- return Number.isFinite(parsed) ? parsed : undefined;
578
- }
579
-
580
- function retryAfterHeaderMs(value: string | null): number | undefined {
581
- if (!value) return undefined;
582
- const seconds = Number(value);
583
- if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
584
- const date = Date.parse(value);
585
- if (!Number.isFinite(date)) return undefined;
586
- return Math.max(0, date - Date.now());
587
- }
588
-
589
- function codexRetryAfterMs(error: unknown): number | undefined {
590
- const message = errMessage(error);
591
- if (!/\b429\b/.test(message)) return undefined;
592
- return retryAfterHeaderMs(retryAfterFromText(message)) ?? 60_000;
593
- }
594
-
595
- function retryAfterFromText(message: string): string | null {
596
- return /retry-after[:=]\s*([^,;\s]+)/i.exec(message)?.[1] ?? null;
597
- }
598
-
599
- function numberValue(value: unknown): number | undefined {
600
- if (typeof value === "number" && Number.isFinite(value)) return value;
601
- if (typeof value === "string" && value.trim()) {
602
- const parsed = Number(value);
603
- return Number.isFinite(parsed) ? parsed : undefined;
604
- }
605
- return undefined;
606
- }
1
+ export {
2
+ CLAUDE_OAUTH_BETA,
3
+ CLAUDE_USAGE_URL,
4
+ QUOTA_FAILURE_LOG_INTERVAL_MS,
5
+ QUOTA_FAST_RETRY_MS,
6
+ QUOTA_POLL_INTERVAL_MS,
7
+ QuotaCollectionError,
8
+ QuotaRetryAfterError,
9
+ claudeUsageQuotaState,
10
+ collectClaudeQuotaSample,
11
+ collectClaudeQuotaState,
12
+ collectCodexQuotaSample,
13
+ collectCodexQuotaState,
14
+ codexRateLimitsQuotaState,
15
+ credentialAccountKey,
16
+ defaultClaudeConfigDir,
17
+ defaultClaudeCredentialsPath,
18
+ hostProviderAccountKey,
19
+ providerPayloadAccountKey,
20
+ providerQuotaErrorFromCollectorError,
21
+ quotaRetryAfterMs,
22
+ readClaudeOAuthAccessToken,
23
+ resolveClaudeQuotaIdentity,
24
+ resolveCodexQuotaIdentity,
25
+ resolveCodexQuotaIdentityFromHome,
26
+ resolveStableClaudeQuotaIdentity,
27
+ resolveStableCodexQuotaIdentity,
28
+ resolveStableCodexQuotaIdentityFromHome,
29
+ type ProviderQuotaIdentity,
30
+ type ProviderQuotaSample,
31
+ } from "agent-relay-sdk/provider-quota";
@@ -24,7 +24,6 @@ import { RunnerInsights } from "./runner-insights";
24
24
  import { BusyReconciler } from "./busy-reconciler";
25
25
  import { publishCapturedResponse } from "./response-capture-report";
26
26
  import { runnerLaunchInjectionSessionEvents, type RunnerRelayInjectionEvent } from "./relay-injection-events";
27
- import { RunnerQuotaPoller } from "./quota";
28
27
  import { capsFromEnv, contextStateDirFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, registrationTimeoutMsFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
29
28
  import {
30
29
  appliedAgentProfileMetadata,
@@ -205,7 +204,6 @@ export class AgentRunner {
205
204
  private readonly sessionOutbox: Outbox;
206
205
  private readonly insights: RunnerInsights;
207
206
  private readonly busyReconciler: BusyReconciler;
208
- private readonly quotaPoller: RunnerQuotaPoller;
209
207
  private currentToken?: string;
210
208
  private currentTokenJti?: string;
211
209
  private currentTokenProfileId?: string;
@@ -353,7 +351,6 @@ export class AgentRunner {
353
351
  clearProviderTurn: (reason) => this.forceClearProviderTurn(reason),
354
352
  sessionDebug: (message) => this.sessionDebug(message),
355
353
  });
356
- this.quotaPoller = new RunnerQuotaPoller({ provider: options.provider, agentId: this.agentId, getProcess: () => this.process, onUpdate: () => this.publishStatus(), log: (message) => this.logRunnerDiagnostic(message), reportProviderQuota: (input) => this.http.upsertProviderQuota(input).then(() => {}) });
357
354
  this.bus = new RelayBusClient({
358
355
  url: relayBusUrl(options.relayUrl),
359
356
  role: "provider",
@@ -464,7 +461,7 @@ export class AgentRunner {
464
461
  void this.sweepStaleScratch();
465
462
  this.process = await this.spawnProvider();
466
463
  this.writeRunnerInfoFile();
467
- this.processStartedAt = Date.now(); this.quotaPoller.start();
464
+ this.processStartedAt = Date.now();
468
465
  this.publishStatus();
469
466
  await this.deliverInitialPrompt();
470
467
  await this.bootstrapUnreadMessages();
@@ -496,7 +493,7 @@ export class AgentRunner {
496
493
  if (this.httpLivenessTimer) clearInterval(this.httpLivenessTimer);
497
494
  this.httpLivenessTimer = undefined;
498
495
  if (this.tokenRenewTimer) clearTimeout(this.tokenRenewTimer);
499
- this.tokenRenewTimer = undefined; this.quotaPoller.stop();
496
+ this.tokenRenewTimer = undefined;
500
497
  this.busyReconciler.disarm();
501
498
  this.stopReasoningTail();
502
499
  this.obligationCache.stop();
@@ -937,7 +934,6 @@ export class AgentRunner {
937
934
  if (this.stopped) return;
938
935
  this.process = await this.spawnProvider();
939
936
  this.processStartedAt = Date.now();
940
- this.quotaPoller.clear(); this.quotaPoller.start();
941
937
  } finally {
942
938
  this.restartInProgress = false;
943
939
  }
@@ -2114,7 +2110,7 @@ export class AgentRunner {
2114
2110
  const probeMetrics = this.latestProbeMetrics();
2115
2111
  const probeContext = probeMetrics ? contextStateFromProbeMetrics(probeMetrics) : undefined;
2116
2112
  const context = processContext ?? probeContext;
2117
- const quota = this.quotaPoller.current ?? (probeMetrics ? quotaStateFromProbeMetrics(probeMetrics) : undefined);
2113
+ const quota = probeMetrics ? quotaStateFromProbeMetrics(probeMetrics) : undefined;
2118
2114
  const terminalSession = this.providerTerminalSession();
2119
2115
  const terminalSocket = this.providerTerminalSocket();
2120
2116
  const probeModel: ProbeModelInfo | undefined = probeMetrics?.model || probeMetrics?.effort