agent-relay-runner 0.76.0 → 0.77.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.76.0",
3
+ "version": "0.77.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.51"
23
+ "agent-relay-sdk": "0.2.52"
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.76.0",
4
+ "version": "0.77.1",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -47,7 +47,7 @@ export class ClaudeAdapter implements ProviderAdapter {
47
47
  stderr: "inherit",
48
48
  });
49
49
  void proc.exited.then((code) => this.statusCb(code === 0 ? "offline" : "error"));
50
- return { pid: proc.pid, process: proc, meta: { monitor: config.monitor } };
50
+ return { pid: proc.pid, process: proc, meta: { monitor: config.monitor, env: args.env } };
51
51
  }
52
52
 
53
53
  async shutdown(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void> {
@@ -335,9 +335,8 @@ export class ClaudeAdapter implements ProviderAdapter {
335
335
  }
336
336
 
337
337
  return {
338
- pid: undefined,
339
- process: undefined,
340
- meta: {
338
+ pid: undefined, process: undefined, meta: {
339
+ env: spawnArgs.env,
341
340
  monitor: config.monitor,
342
341
  // When the profile disables the relay plugin, the bundled monitor never
343
342
  // loads, so no monitor will ever connect — deliver() must use tmux injection.
package/src/quota.ts CHANGED
@@ -1,25 +1,52 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import type { ContextProbeMetrics, QuotaState, QuotaWindowName } from "agent-relay-sdk";
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";
5
7
  import { errMessage, isRecord, quotaStateFromProbeMetrics, stringValue } from "agent-relay-sdk";
8
+ import { sanitizeFsName } from "agent-relay-sdk/fs-name";
6
9
  import type { ManagedProcess } from "./adapter";
10
+ import { agentRelayHome, runnerOutboxDirWithInfoFallback } from "./config";
7
11
 
8
12
  const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
9
13
  const CLAUDE_OAUTH_BETA = "oauth-2025-04-20";
10
- const QUOTA_POLL_INTERVAL_MS = 15 * 60 * 1000;
14
+ const QUOTA_POLL_INTERVAL_MS = 5 * 60 * 1000;
15
+ const QUOTA_FAST_RETRY_MS = 15 * 1000;
11
16
  const QUOTA_FAILURE_LOG_INTERVAL_MS = 5 * 60 * 1000;
17
+ const QUOTA_LOCK_STALE_MS = 60 * 1000;
12
18
 
13
19
  type CodexQuotaClient = {
14
20
  isConnected?: () => boolean;
15
21
  accountRateLimitsRead?: () => Promise<unknown>;
16
22
  };
17
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
+
18
44
  export class RunnerQuotaPoller {
19
45
  private quotaState?: QuotaState;
20
46
  private timer?: Timer;
21
47
  private inFlight = false;
22
48
  private active = false;
49
+ private activeLockDir?: string;
23
50
  private lastLog?: { key: string; at: number };
24
51
 
25
52
  constructor(private readonly options: {
@@ -28,6 +55,9 @@ export class RunnerQuotaPoller {
28
55
  getProcess: () => ManagedProcess | undefined;
29
56
  onUpdate: () => void;
30
57
  log: (message: string) => void;
58
+ reportProviderQuota?: (input: ProviderQuotaUpdateInput) => Promise<unknown>;
59
+ quotaStateDir?: string;
60
+ fetchImpl?: typeof fetch;
31
61
  }) {}
32
62
 
33
63
  get current(): QuotaState | undefined {
@@ -45,6 +75,9 @@ export class RunnerQuotaPoller {
45
75
  this.active = false;
46
76
  if (this.timer) clearTimeout(this.timer);
47
77
  this.timer = undefined;
78
+ const lockDir = this.activeLockDir;
79
+ this.activeLockDir = undefined;
80
+ if (lockDir) releaseQuotaLock(lockDir);
48
81
  }
49
82
 
50
83
  clear(): void {
@@ -64,26 +97,95 @@ export class RunnerQuotaPoller {
64
97
  private async refresh(): Promise<void> {
65
98
  if (this.inFlight) return;
66
99
  this.inFlight = true;
100
+ const now = Date.now();
101
+ let identity: QuotaIdentity | undefined;
102
+ let lockDir: string | undefined;
67
103
  try {
68
- const quota = await collectRunnerQuotaState({
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({
69
121
  provider: this.options.provider,
70
122
  agentId: this.options.agentId,
71
- process: this.options.getProcess(),
123
+ process,
124
+ credentialsPath: identity.credentialsPath,
125
+ fetchImpl: this.options.fetchImpl,
72
126
  });
73
- if (quota && this.active) {
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);
74
146
  this.quotaState = quota;
75
147
  this.options.onUpdate();
76
148
  }
77
149
  this.schedule();
78
150
  } catch (error) {
79
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
+ }
80
173
  if (this.active) this.logFailure(error, retryAfterMs);
81
- this.schedule(retryAfterMs ?? QUOTA_POLL_INTERVAL_MS);
174
+ this.schedule(retryDelayMs);
82
175
  } finally {
176
+ if (lockDir && this.activeLockDir === lockDir) {
177
+ releaseQuotaLock(lockDir);
178
+ this.activeLockDir = undefined;
179
+ }
83
180
  this.inFlight = false;
84
181
  }
85
182
  }
86
183
 
184
+ private async reportProviderQuota(input: ProviderQuotaUpdateInput): Promise<void> {
185
+ if (!this.options.reportProviderQuota) return;
186
+ await this.options.reportProviderQuota(input);
187
+ }
188
+
87
189
  private logFailure(error: unknown, retryAfterMs: number | undefined): void {
88
190
  const key = retryAfterMs !== undefined ? `retry-after:${retryAfterMs}` : errMessage(error);
89
191
  const now = Date.now();
@@ -101,31 +203,90 @@ export class QuotaRetryAfterError extends Error {
101
203
  }
102
204
  }
103
205
 
104
- export function quotaRetryAfterMs(error: unknown): number | undefined {
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 {
105
214
  return error instanceof QuotaRetryAfterError ? error.retryAfterMs : undefined;
106
215
  }
107
216
 
108
- export async function collectRunnerQuotaState(input: {
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: {
109
247
  provider: string;
110
248
  agentId: string;
111
249
  process?: ManagedProcess;
112
- }): Promise<QuotaState | undefined> {
113
- if (input.provider === "claude") return collectClaudeQuotaState({ agentId: input.agentId });
114
- if (input.provider !== "codex") return undefined;
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 {};
115
255
  const client = input.process?.meta?.client as CodexQuotaClient | undefined;
116
- if (!client?.isConnected?.() || typeof client.accountRateLimitsRead !== "function") return undefined;
117
- return collectCodexQuotaState({
256
+ if (!client?.isConnected?.() || typeof client.accountRateLimitsRead !== "function") return {};
257
+ return collectCodexQuotaSample({
118
258
  agentId: codexProcessAgentId(input.process, input.agentId),
119
259
  rateLimitsRead: () => client.accountRateLimitsRead!(),
120
260
  });
121
261
  }
122
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
+
123
277
  export async function collectClaudeQuotaState(options: {
124
278
  agentId: string;
125
279
  credentialsPath?: string;
126
280
  fetchImpl?: typeof fetch;
127
281
  now?: number;
128
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> {
129
290
  const accessToken = await readClaudeOAuthAccessToken(options.credentialsPath ?? defaultClaudeCredentialsPath());
130
291
  if (!accessToken) return undefined;
131
292
  const fetchImpl = options.fetchImpl ?? fetch;
@@ -140,8 +301,9 @@ export async function collectClaudeQuotaState(options: {
140
301
  if (response.status === 429) {
141
302
  throw new QuotaRetryAfterError(retryAfterHeaderMs(response.headers.get("retry-after")) ?? 60_000, "Claude quota source returned 429");
142
303
  }
143
- if (!response.ok) return undefined;
144
- return claudeUsageQuotaState(await response.json(), options.agentId, options.now ?? Date.now());
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();
145
307
  }
146
308
 
147
309
  export function claudeUsageQuotaState(payload: unknown, agentId: string, now = Date.now()): QuotaState | undefined {
@@ -163,8 +325,20 @@ export async function collectCodexQuotaState(input: {
163
325
  rateLimitsRead: () => Promise<unknown>;
164
326
  now?: number;
165
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> {
166
336
  try {
167
- return codexRateLimitsQuotaState(await input.rateLimitsRead(), input.agentId, input.now ?? Date.now());
337
+ const payload = await input.rateLimitsRead();
338
+ return {
339
+ quota: codexRateLimitsQuotaState(payload, input.agentId, input.now ?? Date.now()),
340
+ accountKey: providerPayloadAccountKey(payload),
341
+ };
168
342
  } catch (error) {
169
343
  const retryAfter = codexRetryAfterMs(error);
170
344
  if (retryAfter !== undefined) throw new QuotaRetryAfterError(retryAfter, "Codex quota source returned 429");
@@ -190,7 +364,156 @@ export function codexRateLimitsQuotaState(payload: unknown, agentId: string, now
190
364
  }
191
365
 
192
366
  function defaultClaudeCredentialsPath(): string {
193
- return join(homedir(), ".claude", ".credentials.json");
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);
194
517
  }
195
518
 
196
519
  function quotaProviderSupported(provider: string): boolean {
@@ -353,7 +353,7 @@ export class AgentRunner {
353
353
  clearProviderTurn: (reason) => this.forceClearProviderTurn(reason),
354
354
  sessionDebug: (message) => this.sessionDebug(message),
355
355
  });
356
- this.quotaPoller = new RunnerQuotaPoller({ provider: options.provider, agentId: this.agentId, getProcess: () => this.process, onUpdate: () => this.publishStatus(), log: (message) => this.logRunnerDiagnostic(message) });
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
357
  this.bus = new RelayBusClient({
358
358
  url: relayBusUrl(options.relayUrl),
359
359
  role: "provider",
@@ -2126,7 +2126,6 @@ export class AgentRunner {
2126
2126
  ...(terminalSocket ? { tmuxSocket: terminalSocket } : {}),
2127
2127
  };
2128
2128
  if (context) meta.context = context;
2129
- if (quota) meta.quota = quota;
2130
2129
  return meta;
2131
2130
  }
2132
2131