agent-relay-orchestrator 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-orchestrator",
3
- "version": "0.77.1",
3
+ "version": "0.78.1",
4
4
  "description": "Agent Relay orchestrator — manages agent lifecycle across hosts",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "test": "bun test"
17
17
  },
18
18
  "dependencies": {
19
- "agent-relay-sdk": "0.2.52"
19
+ "agent-relay-sdk": "0.2.54"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/bun": "latest",
package/src/config.ts CHANGED
@@ -42,6 +42,14 @@ export function bunBinFromEnv(): string | undefined {
42
42
  return process.env.AGENT_RELAY_BUN_BIN;
43
43
  }
44
44
 
45
+ export function codexCommandFromEnv(): string | undefined {
46
+ return process.env.AGENT_RELAY_CODEX_COMMAND;
47
+ }
48
+
49
+ export function providerHomeRootFromEnv(): string {
50
+ return process.env.AGENT_RELAY_PROVIDER_HOME_ROOT || join(homedir(), ".agent-relay", "provider-homes");
51
+ }
52
+
45
53
  export function disableSystemdSupervisor(): boolean {
46
54
  return process.env.AGENT_RELAY_DISABLE_SYSTEMD_SUPERVISOR === "1";
47
55
  }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import { recoverManagedAgents } from "./recovery";
9
9
  import { ProviderProbeCache } from "./provider-probe";
10
10
  import { sweepEmptyWorkspaceContainers, workspacesRoot } from "./workspace-probe";
11
11
  import { startOrchestratorMaintenanceScheduler } from "./maintenance";
12
+ import { OrchestratorQuotaPoller } from "./quota-poller";
12
13
 
13
14
  const args = process.argv.slice(2);
14
15
 
@@ -50,6 +51,7 @@ const config = loadConfig();
50
51
  const probeCache = new ProviderProbeCache(config);
51
52
  const relay = createRelayClient(config, probeCache);
52
53
  const control = createControlHandler(config, relay);
54
+ const quotaPoller = new OrchestratorQuotaPoller(config, relay);
53
55
 
54
56
  const POLL_INTERVAL_MS = 3_000;
55
57
  const REGISTER_RETRY_MS = 5_000;
@@ -81,6 +83,7 @@ async function startup(): Promise<void> {
81
83
 
82
84
  // Host-local maintenance must run where the agent processes and tmux sockets live.
83
85
  startOrchestratorMaintenanceScheduler();
86
+ quotaPoller.start();
84
87
 
85
88
  // Sweep empty workspace container dirs left behind by prior cleanups (#280).
86
89
  const swept = sweepEmptyWorkspaceContainers(workspacesRoot(config.baseDir));
@@ -207,6 +210,7 @@ async function shutdown(): Promise<void> {
207
210
  if (healthCheckTimer) clearInterval(healthCheckTimer);
208
211
  if (guestReaperTimer) clearInterval(guestReaperTimer);
209
212
  if (apiServer) apiServer.stop();
213
+ quotaPoller.stop();
210
214
  relay.stopHeartbeatLoop();
211
215
  process.exit(0);
212
216
  }
@@ -0,0 +1,446 @@
1
+ import { homedir } from "node:os";
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { createServer } from "node:net";
4
+ import { join } from "node:path";
5
+ import {
6
+ QUOTA_FAILURE_LOG_INTERVAL_MS,
7
+ QUOTA_FAST_RETRY_MS,
8
+ QUOTA_POLL_INTERVAL_MS,
9
+ QuotaCollectionError,
10
+ collectClaudeQuotaSample,
11
+ collectCodexQuotaSample,
12
+ providerQuotaErrorFromCollectorError,
13
+ quotaRetryAfterMs,
14
+ resolveStableClaudeQuotaIdentity,
15
+ resolveStableCodexQuotaIdentityFromHome,
16
+ type ProviderQuotaIdentity,
17
+ type ProviderQuotaSample,
18
+ } from "agent-relay-sdk/provider-quota";
19
+ import type { ProviderQuotaLeaseAcquireInput, ProviderQuotaUpdateInput } from "agent-relay-sdk";
20
+ import { errMessage } from "agent-relay-sdk";
21
+ import { codexCommandFromEnv, providerHomeRootFromEnv, type OrchestratorConfig } from "./config";
22
+
23
+ const QUOTA_LEASE_TTL_MS = 90_000;
24
+ const QUOTA_LEASE_RENEW_MS = 30_000;
25
+ const CODEX_APP_SERVER_CONNECT_ATTEMPTS = 40;
26
+ const CODEX_APP_SERVER_CONNECT_RETRY_MS = 250;
27
+
28
+ type QuotaRelay = {
29
+ acquireProviderQuotaLease(orchestratorId: string, input: ProviderQuotaLeaseAcquireInput): Promise<{
30
+ acquired: boolean;
31
+ lease?: { leaseToken: string; expiresAt: number };
32
+ retryAfterMs?: number;
33
+ }>;
34
+ releaseProviderQuotaLease(orchestratorId: string, input: ProviderQuotaLeaseAcquireInput & { leaseToken: string }): Promise<unknown>;
35
+ reportProviderQuota(input: ProviderQuotaUpdateInput): Promise<unknown>;
36
+ connected: boolean;
37
+ };
38
+
39
+ type QuotaCandidate = ProviderQuotaIdentity & {
40
+ appServerUrl?: string;
41
+ codexHome?: string;
42
+ };
43
+
44
+ type QuotaPollState = {
45
+ leaseToken?: string;
46
+ leaseExpiresAt?: number;
47
+ nextPollAt?: number;
48
+ lastAttemptAt?: number;
49
+ lastLog?: { key: string; at: number };
50
+ };
51
+
52
+ export class OrchestratorQuotaPoller {
53
+ private timer?: Timer;
54
+ private active = false;
55
+ private inFlight = false;
56
+ private readonly states = new Map<string, QuotaPollState>();
57
+ private readonly logStates = new Map<string, { key: string; at: number }>();
58
+
59
+ constructor(
60
+ private readonly config: OrchestratorConfig,
61
+ private readonly relay: QuotaRelay,
62
+ private readonly options: {
63
+ intervalMs?: number;
64
+ fetchImpl?: typeof fetch;
65
+ now?: () => number;
66
+ log?: (message: string) => void;
67
+ codexRateLimitsRead?: (appServerUrl: string) => Promise<unknown>;
68
+ } = {},
69
+ ) {}
70
+
71
+ start(): void {
72
+ if (this.active) return;
73
+ this.active = true;
74
+ this.schedule(1_000);
75
+ }
76
+
77
+ stop(): void {
78
+ this.active = false;
79
+ if (this.timer) clearTimeout(this.timer);
80
+ this.timer = undefined;
81
+ for (const [key, state] of this.states) {
82
+ if (!state.leaseToken) continue;
83
+ const [provider, accountKey] = splitStateKey(key);
84
+ void this.relay.releaseProviderQuotaLease(this.config.id, { provider, accountKey, leaseToken: state.leaseToken }).catch(() => {});
85
+ }
86
+ this.states.clear();
87
+ this.logStates.clear();
88
+ }
89
+
90
+ async tick(): Promise<void> {
91
+ if (this.inFlight || !this.active || !this.relay.connected) return;
92
+ this.inFlight = true;
93
+ try {
94
+ const candidates = await this.discoverCandidates();
95
+ await this.releaseRemovedCandidates(candidates);
96
+ for (const candidate of candidates) {
97
+ await this.processCandidate(candidate);
98
+ }
99
+ } finally {
100
+ this.inFlight = false;
101
+ this.schedule(this.options.intervalMs ?? QUOTA_LEASE_RENEW_MS);
102
+ }
103
+ }
104
+
105
+ private schedule(delayMs: number): void {
106
+ if (this.timer) clearTimeout(this.timer);
107
+ this.timer = undefined;
108
+ if (!this.active) return;
109
+ this.timer = setTimeout(() => {
110
+ this.timer = undefined;
111
+ void this.tick();
112
+ }, Math.max(1_000, delayMs));
113
+ }
114
+
115
+ private async discoverCandidates(): Promise<QuotaCandidate[]> {
116
+ const candidates: QuotaCandidate[] = [];
117
+ if (this.config.providers.includes("claude")) {
118
+ candidates.push(...await this.discoverClaudeCandidates());
119
+ }
120
+ if (this.config.providers.includes("codex")) {
121
+ candidates.push(...await this.discoverCodexCandidates());
122
+ }
123
+ const deduped = new Map<string, QuotaCandidate>();
124
+ for (const candidate of candidates) {
125
+ deduped.set(candidateStateKey(candidate), candidate);
126
+ }
127
+ return [...deduped.values()];
128
+ }
129
+
130
+ private async discoverClaudeCandidates(): Promise<QuotaCandidate[]> {
131
+ const paths = [
132
+ join(this.config.env.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), ".credentials.json"),
133
+ ...providerHomeCredentialPaths("claude", ".credentials.json"),
134
+ ];
135
+ const candidates: QuotaCandidate[] = [];
136
+ for (const credentialsPath of paths) {
137
+ const identity = await resolveStableClaudeQuotaIdentity({ credentialsPath });
138
+ if (identity) candidates.push(identity);
139
+ }
140
+ if (candidates.length === 0) {
141
+ this.logOnce("claude:no-stable-credentials", "quota refresh skipped for claude: no materialized Claude OAuth credentials found");
142
+ }
143
+ return candidates;
144
+ }
145
+
146
+ private async discoverCodexCandidates(): Promise<QuotaCandidate[]> {
147
+ const homes = [
148
+ this.config.env.CODEX_HOME || process.env.CODEX_HOME || join(homedir(), ".codex"),
149
+ ...providerHomeConfigDirs("codex", "auth.json"),
150
+ ];
151
+ const appServerUrl = this.config.env.CODEX_APP_SERVER_URL || process.env.CODEX_APP_SERVER_URL;
152
+ const candidates: QuotaCandidate[] = [];
153
+ for (const codexHome of homes) {
154
+ const identity = await resolveStableCodexQuotaIdentityFromHome({ codexHome });
155
+ if (identity) candidates.push({ ...identity, codexHome, ...(appServerUrl ? { appServerUrl } : {}) });
156
+ }
157
+ if (candidates.length === 0) {
158
+ this.logOnce("codex:no-stable-auth", "quota refresh skipped for codex: no account id found in Codex auth.json");
159
+ }
160
+ return candidates;
161
+ }
162
+
163
+ private async releaseRemovedCandidates(candidates: QuotaCandidate[]): Promise<void> {
164
+ const live = new Set(candidates.map(candidateStateKey));
165
+ for (const [key, state] of this.states) {
166
+ if (live.has(key)) continue;
167
+ this.states.delete(key);
168
+ if (!state.leaseToken) continue;
169
+ const [provider, accountKey] = splitStateKey(key);
170
+ await this.relay.releaseProviderQuotaLease(this.config.id, { provider, accountKey, leaseToken: state.leaseToken }).catch(() => {});
171
+ }
172
+ }
173
+
174
+ private async processCandidate(candidate: QuotaCandidate): Promise<void> {
175
+ const state = this.stateFor(candidate);
176
+ const now = this.now();
177
+ if (!await this.ensureLease(candidate, state, now)) return;
178
+ if (state.nextPollAt !== undefined && state.nextPollAt > now) return;
179
+
180
+ const lastAttemptAt = now;
181
+ try {
182
+ const sample = await this.collect(candidate);
183
+ const quota = sample.quota;
184
+ if (!quota) throw new QuotaCollectionError("creds_not_ready", `${candidate.provider} quota source is not ready`);
185
+ const update: ProviderQuotaUpdateInput = {
186
+ provider: candidate.provider,
187
+ accountKey: sample.accountKey ?? candidate.accountKey,
188
+ quota,
189
+ lastAttemptAt: quota.updatedAt,
190
+ sourceAgentId: this.sourceAgentId(),
191
+ };
192
+ await this.relay.reportProviderQuota(update);
193
+ state.lastAttemptAt = update.lastAttemptAt;
194
+ state.nextPollAt = now + QUOTA_POLL_INTERVAL_MS;
195
+ } catch (error) {
196
+ const retryAfterMs = quotaRetryAfterMs(error);
197
+ const lastError = providerQuotaErrorFromCollectorError(error, retryAfterMs);
198
+ const retryDelayMs = retryAfterMs ?? (state.lastAttemptAt ? QUOTA_POLL_INTERVAL_MS : QUOTA_FAST_RETRY_MS);
199
+ state.lastAttemptAt = lastAttemptAt;
200
+ state.nextPollAt = now + retryDelayMs;
201
+ await this.relay.reportProviderQuota({
202
+ provider: candidate.provider,
203
+ accountKey: candidate.accountKey,
204
+ lastAttemptAt,
205
+ lastError,
206
+ sourceAgentId: this.sourceAgentId(),
207
+ }).catch((publishError) => this.log(`quota status publish failed: ${errMessage(publishError)}`));
208
+ this.logFailure(candidate, error, retryAfterMs);
209
+ }
210
+ }
211
+
212
+ private async ensureLease(candidate: QuotaCandidate, state: QuotaPollState, now: number): Promise<boolean> {
213
+ if (state.leaseToken && state.leaseExpiresAt && state.leaseExpiresAt - now > QUOTA_LEASE_RENEW_MS) return true;
214
+ const result = await this.relay.acquireProviderQuotaLease(this.config.id, {
215
+ provider: candidate.provider,
216
+ accountKey: candidate.accountKey,
217
+ ...(state.leaseToken ? { leaseToken: state.leaseToken } : {}),
218
+ ttlMs: QUOTA_LEASE_TTL_MS,
219
+ });
220
+ if (!result.acquired || !result.lease) {
221
+ state.leaseToken = undefined;
222
+ state.leaseExpiresAt = undefined;
223
+ state.nextPollAt = now + Math.min(result.retryAfterMs ?? QUOTA_LEASE_RENEW_MS, QUOTA_LEASE_RENEW_MS);
224
+ return false;
225
+ }
226
+ state.leaseToken = result.lease.leaseToken;
227
+ state.leaseExpiresAt = result.lease.expiresAt;
228
+ return true;
229
+ }
230
+
231
+ private async collect(candidate: QuotaCandidate): Promise<ProviderQuotaSample> {
232
+ if (candidate.provider === "claude") {
233
+ return collectClaudeQuotaSample({
234
+ agentId: this.sourceAgentId(),
235
+ credentialsPath: candidate.credentialsPath,
236
+ fetchImpl: this.options.fetchImpl,
237
+ });
238
+ }
239
+ if (candidate.provider === "codex") {
240
+ return collectCodexQuotaSample({
241
+ agentId: this.sourceAgentId(),
242
+ rateLimitsRead: () => this.options.codexRateLimitsRead
243
+ ? this.options.codexRateLimitsRead(candidate.appServerUrl ?? "")
244
+ : candidate.appServerUrl
245
+ ? codexRateLimitsRead(candidate.appServerUrl)
246
+ : codexRateLimitsReadFromHome(candidate.codexHome),
247
+ });
248
+ }
249
+ return {};
250
+ }
251
+
252
+ private stateFor(candidate: QuotaCandidate): QuotaPollState {
253
+ const key = candidateStateKey(candidate);
254
+ const existing = this.states.get(key);
255
+ if (existing) return existing;
256
+ const created: QuotaPollState = {};
257
+ this.states.set(key, created);
258
+ return created;
259
+ }
260
+
261
+ private sourceAgentId(): string {
262
+ return `orchestrator-${this.config.id}`;
263
+ }
264
+
265
+ private now(): number {
266
+ return this.options.now?.() ?? Date.now();
267
+ }
268
+
269
+ private log(message: string): void {
270
+ (this.options.log ?? ((line) => console.error(`[orchestrator] ${line}`)))(message);
271
+ }
272
+
273
+ private logFailure(candidate: QuotaCandidate, error: unknown, retryAfterMs: number | undefined): void {
274
+ const state = this.stateFor(candidate);
275
+ const key = retryAfterMs !== undefined ? `retry-after:${retryAfterMs}` : errMessage(error);
276
+ const now = this.now();
277
+ if (state.lastLog?.key === key && now - state.lastLog.at < QUOTA_FAILURE_LOG_INTERVAL_MS) return;
278
+ state.lastLog = { key, at: now };
279
+ const suffix = retryAfterMs !== undefined ? `; retrying in ${Math.round(retryAfterMs / 1000)}s` : "";
280
+ this.log(`quota refresh failed for ${candidate.provider}/${candidate.accountKey}${suffix}: ${errMessage(error)}`);
281
+ }
282
+
283
+ private logOnce(key: string, message: string): void {
284
+ const lastLog = this.logStates.get(key);
285
+ const now = this.now();
286
+ if (lastLog && now - lastLog.at < QUOTA_FAILURE_LOG_INTERVAL_MS) return;
287
+ this.logStates.set(key, { key, at: now });
288
+ this.log(message);
289
+ }
290
+ }
291
+
292
+ async function codexRateLimitsRead(appServerUrl: string, attempts = 1): Promise<unknown> {
293
+ const client = new JsonRpcWebSocket(appServerUrl);
294
+ try {
295
+ await connectWithRetry(client, attempts);
296
+ await client.request("initialize", {
297
+ clientInfo: { name: "agent-relay-orchestrator", title: "Agent Relay Orchestrator" },
298
+ capabilities: { experimentalApi: true },
299
+ }).catch(() => undefined);
300
+ return await client.request("account/rateLimits/read");
301
+ } finally {
302
+ client.close();
303
+ }
304
+ }
305
+
306
+ async function codexRateLimitsReadFromHome(codexHome: string | undefined): Promise<unknown> {
307
+ if (!codexHome) throw new QuotaCollectionError("creds_not_ready", "Codex home is not configured");
308
+ const appServerUrl = await freeLoopbackWsUrl();
309
+ const command = codexCommandFromEnv() || "codex";
310
+ const proc = Bun.spawn([command, "app-server", "--listen", appServerUrl], {
311
+ env: {
312
+ ...process.env,
313
+ CODEX_HOME: codexHome,
314
+ CODEX_APP_SERVER_URL: appServerUrl,
315
+ },
316
+ stdin: "ignore",
317
+ stdout: "ignore",
318
+ stderr: "ignore",
319
+ });
320
+ try {
321
+ return await codexRateLimitsRead(appServerUrl, CODEX_APP_SERVER_CONNECT_ATTEMPTS);
322
+ } finally {
323
+ proc.kill();
324
+ await Promise.race([proc.exited.catch(() => undefined), Bun.sleep(2_000)]).catch(() => undefined);
325
+ }
326
+ }
327
+
328
+ async function connectWithRetry(client: JsonRpcWebSocket, attempts: number): Promise<void> {
329
+ let lastError: unknown;
330
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
331
+ try {
332
+ await client.connect();
333
+ return;
334
+ } catch (error) {
335
+ lastError = error;
336
+ await Bun.sleep(CODEX_APP_SERVER_CONNECT_RETRY_MS);
337
+ }
338
+ }
339
+ throw lastError ?? new Error("websocket connect failed");
340
+ }
341
+
342
+ async function freeLoopbackWsUrl(): Promise<string> {
343
+ const port = await new Promise<number>((resolve, reject) => {
344
+ const server = createServer();
345
+ server.on("error", reject);
346
+ server.listen(0, "127.0.0.1", () => {
347
+ const address = server.address();
348
+ const port = typeof address === "object" && address ? address.port : undefined;
349
+ server.close(() => port ? resolve(port) : reject(new Error("failed to reserve loopback port")));
350
+ });
351
+ });
352
+ return `ws://127.0.0.1:${port}`;
353
+ }
354
+
355
+ function providerHomeRoot(): string {
356
+ return providerHomeRootFromEnv();
357
+ }
358
+
359
+ function providerHomeConfigDirs(provider: "claude" | "codex", markerFile: string): string[] {
360
+ const root = join(providerHomeRoot(), provider);
361
+ const dirs: string[] = [];
362
+ for (const profile of safeReadDir(root)) {
363
+ const profileDir = join(root, profile);
364
+ for (const instance of safeReadDir(profileDir)) {
365
+ const dir = join(profileDir, instance);
366
+ if (existsSync(join(dir, markerFile))) dirs.push(dir);
367
+ }
368
+ }
369
+ return dirs;
370
+ }
371
+
372
+ function providerHomeCredentialPaths(provider: "claude", markerFile: string): string[] {
373
+ return providerHomeConfigDirs(provider, markerFile).map((dir) => join(dir, markerFile));
374
+ }
375
+
376
+ function safeReadDir(path: string): string[] {
377
+ try {
378
+ return readdirSync(path, { withFileTypes: true })
379
+ .filter((entry) => entry.isDirectory())
380
+ .map((entry) => entry.name);
381
+ } catch {
382
+ return [];
383
+ }
384
+ }
385
+
386
+ class JsonRpcWebSocket {
387
+ private ws!: WebSocket;
388
+ private nextId = 1;
389
+ private pending = new Map<number, { resolve: (value: unknown) => void; reject: (error: unknown) => void }>();
390
+
391
+ constructor(private readonly url: string) {}
392
+
393
+ async connect(): Promise<void> {
394
+ await new Promise<void>((resolve, reject) => {
395
+ const ws = new WebSocket(this.url);
396
+ this.ws = ws;
397
+ ws.onopen = () => resolve();
398
+ ws.onerror = () => reject(new Error("websocket error"));
399
+ ws.onclose = (event) => {
400
+ const error = new Error(`websocket closed code=${event.code} reason=${event.reason || "(none)"}`);
401
+ for (const pending of this.pending.values()) pending.reject(error);
402
+ this.pending.clear();
403
+ };
404
+ ws.onmessage = (event) => this.handleMessage(String(event.data));
405
+ });
406
+ }
407
+
408
+ request(method: string, params?: unknown): Promise<unknown> {
409
+ const id = this.nextId++;
410
+ const promise = new Promise<unknown>((resolve, reject) => {
411
+ this.pending.set(id, { resolve, reject });
412
+ });
413
+ this.ws.send(JSON.stringify({ id, method, params }));
414
+ return promise;
415
+ }
416
+
417
+ close(): void {
418
+ this.ws?.close();
419
+ }
420
+
421
+ private handleMessage(raw: string): void {
422
+ let message: unknown;
423
+ try {
424
+ message = JSON.parse(raw) as unknown;
425
+ } catch {
426
+ return;
427
+ }
428
+ if (!message || typeof message !== "object" || !("id" in message)) return;
429
+ const id = Number((message as { id: unknown }).id);
430
+ const pending = this.pending.get(id);
431
+ if (!pending) return;
432
+ this.pending.delete(id);
433
+ const response = message as { result?: unknown; error?: { message?: string; code?: number } };
434
+ if (response.error) pending.reject(new Error(`${response.error.message ?? "JSON-RPC error"} (${response.error.code ?? "unknown"})`));
435
+ else pending.resolve(response.result);
436
+ }
437
+ }
438
+
439
+ function candidateStateKey(candidate: Pick<QuotaCandidate, "provider" | "accountKey">): string {
440
+ return `${candidate.provider}\0${candidate.accountKey}`;
441
+ }
442
+
443
+ function splitStateKey(key: string): [string, string] {
444
+ const [provider, accountKey] = key.split("\0");
445
+ return [provider ?? "", accountKey ?? ""];
446
+ }
package/src/relay.ts CHANGED
@@ -2,7 +2,7 @@ import type { OrchestratorConfig } from "./config";
2
2
  import type { ProviderProbeCache } from "./provider-probe";
3
3
  import { detectSelfSupervision } from "./self-supervision";
4
4
  import { GIT_SHA, ORCHESTRATOR_PROTOCOL_VERSION, VERSION, runtimeMetadata } from "./version";
5
- import type { AgentLifecycle, WorkspaceMetadata, WorkspaceMode, ManagedSessionExitDiagnostics as SdkManagedSessionExitDiagnostics } from "agent-relay-sdk";
5
+ import type { AgentLifecycle, ProviderQuotaLeaseAcquireInput, ProviderQuotaLeaseAcquireResult, ProviderQuotaUpdateInput, WorkspaceMetadata, WorkspaceMode, ManagedSessionExitDiagnostics as SdkManagedSessionExitDiagnostics } from "agent-relay-sdk";
6
6
  import { ReconnectionManager, RelayHttpClient } from "agent-relay-sdk";
7
7
 
8
8
  export interface RelayClient {
@@ -11,6 +11,9 @@ export interface RelayClient {
11
11
  updateManagedAgents(agents: ManagedAgentReport[], exitedAgents?: ManagedSessionExitDiagnostics[]): Promise<void>;
12
12
  pollCommands(): Promise<RelayCommand[]>;
13
13
  updateCommand(commandId: string, status: string, result?: Record<string, unknown>, error?: string): Promise<boolean>;
14
+ acquireProviderQuotaLease(orchestratorId: string, input: ProviderQuotaLeaseAcquireInput): Promise<ProviderQuotaLeaseAcquireResult>;
15
+ releaseProviderQuotaLease(orchestratorId: string, input: ProviderQuotaLeaseAcquireInput & { leaseToken: string }): Promise<{ released: boolean }>;
16
+ reportProviderQuota(input: ProviderQuotaUpdateInput): Promise<unknown>;
14
17
  setApiUrl(url: string): void;
15
18
  startHeartbeatLoop(): void;
16
19
  stopHeartbeatLoop(): void;
@@ -239,6 +242,15 @@ export function createRelayClient(config: OrchestratorConfig, probeCache: Provid
239
242
  return null;
240
243
  }
241
244
  },
245
+ acquireProviderQuotaLease(orchestratorId: string, input: ProviderQuotaLeaseAcquireInput): Promise<ProviderQuotaLeaseAcquireResult> {
246
+ return http.acquireProviderQuotaLease(orchestratorId, input);
247
+ },
248
+ releaseProviderQuotaLease(orchestratorId: string, input: ProviderQuotaLeaseAcquireInput & { leaseToken: string }): Promise<{ released: boolean }> {
249
+ return http.releaseProviderQuotaLease(orchestratorId, input);
250
+ },
251
+ reportProviderQuota(input: ProviderQuotaUpdateInput): Promise<unknown> {
252
+ return http.upsertProviderQuota(input);
253
+ },
242
254
  get connected() { return connected; },
243
255
  };
244
256
  }