agent-relay-orchestrator 0.78.3 → 0.78.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-orchestrator",
3
- "version": "0.78.3",
3
+ "version": "0.78.4",
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.55"
19
+ "agent-relay-sdk": "0.2.56"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/bun": "latest",
@@ -3,13 +3,14 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
3
  import { createServer } from "node:net";
4
4
  import { join } from "node:path";
5
5
  import {
6
+ DEFAULT_PROVIDER_QUOTA_CONFIG,
6
7
  QUOTA_FAILURE_LOG_INTERVAL_MS,
7
8
  QUOTA_FAST_RETRY_MS,
8
- QUOTA_POLL_INTERVAL_MS,
9
9
  QuotaCollectionError,
10
10
  collectClaudeQuotaSample,
11
11
  collectCodexQuotaSample,
12
12
  credentialAccountKey,
13
+ normalizeProviderQuotaConfig,
13
14
  providerQuotaErrorFromCollectorError,
14
15
  quotaRetryAfterMs,
15
16
  readClaudeOAuthAccessToken,
@@ -18,7 +19,7 @@ import {
18
19
  type ProviderQuotaIdentity,
19
20
  type ProviderQuotaSample,
20
21
  } from "agent-relay-sdk/provider-quota";
21
- import type { ProviderQuotaLeaseAcquireInput, ProviderQuotaUpdateInput } from "agent-relay-sdk";
22
+ import type { ProviderQuotaConfig, ProviderQuotaConfigMap, ProviderQuotaLeaseAcquireInput, ProviderQuotaUpdateInput } from "agent-relay-sdk";
22
23
  import { errMessage } from "agent-relay-sdk";
23
24
  import { codexCommandFromEnv, providerHomeRootFromEnv, type OrchestratorConfig } from "./config";
24
25
 
@@ -43,6 +44,7 @@ type QuotaRelay = {
43
44
  }>;
44
45
  releaseProviderQuotaLease(orchestratorId: string, input: ProviderQuotaLeaseAcquireInput & { leaseToken: string }): Promise<unknown>;
45
46
  reportProviderQuota(input: ProviderQuotaUpdateInput): Promise<unknown>;
47
+ getProviderQuotaConfig(): Promise<ProviderQuotaConfigMap>;
46
48
  connected: boolean;
47
49
  };
48
50
 
@@ -69,6 +71,10 @@ export class OrchestratorQuotaPoller {
69
71
  private inFlight = false;
70
72
  private readonly states = new Map<string, QuotaPollState>();
71
73
  private readonly logStates = new Map<string, { key: string; at: number }>();
74
+ // Per-provider quota config (#605), refreshed each tick. Empty until the first
75
+ // successful fetch — configFor() falls back to defaults so a fresh/empty config
76
+ // (or an older relay without the endpoint) preserves today's behavior.
77
+ private quotaConfig: ProviderQuotaConfigMap = {};
72
78
 
73
79
  constructor(
74
80
  private readonly config: OrchestratorConfig,
@@ -105,6 +111,7 @@ export class OrchestratorQuotaPoller {
105
111
  if (this.inFlight || !this.active || !this.relay.connected) return;
106
112
  this.inFlight = true;
107
113
  try {
114
+ await this.refreshQuotaConfig();
108
115
  const { candidates, skips } = await this.discoverCandidates();
109
116
  await this.releaseRemovedCandidates(candidates);
110
117
  for (const candidate of candidates) {
@@ -129,15 +136,34 @@ export class OrchestratorQuotaPoller {
129
136
  }, Math.max(1_000, delayMs));
130
137
  }
131
138
 
139
+ // Refresh per-provider quota config (#605). Best-effort: on failure we keep the
140
+ // last known config (defaults for any unset provider), so a transient relay blip
141
+ // never silently stops collection.
142
+ private async refreshQuotaConfig(): Promise<void> {
143
+ try {
144
+ this.quotaConfig = await this.relay.getProviderQuotaConfig();
145
+ } catch {
146
+ // keep prior config; configFor() defaults any missing provider.
147
+ }
148
+ }
149
+
150
+ private configFor(provider: string): ProviderQuotaConfig {
151
+ const stored = this.quotaConfig[provider];
152
+ return stored ? normalizeProviderQuotaConfig(stored) : { ...DEFAULT_PROVIDER_QUOTA_CONFIG };
153
+ }
154
+
132
155
  private async discoverCandidates(): Promise<QuotaDiscovery> {
133
156
  const candidates: QuotaCandidate[] = [];
134
157
  const skips: ProviderSkip[] = [];
135
- if (this.config.providers.includes("claude")) {
158
+ // A disabled provider (#605) is collected from at all: no discovery → no
159
+ // polling/API calls, leases released by releaseRemovedCandidates, and no
160
+ // skip-marker row (disabled is intentional, not a credential failure).
161
+ if (this.config.providers.includes("claude") && this.configFor("claude").enabled) {
136
162
  const found = await this.discoverClaudeCandidates();
137
163
  candidates.push(...found.candidates);
138
164
  if (found.skipReason) skips.push({ provider: "claude", reason: found.skipReason });
139
165
  }
140
- if (this.config.providers.includes("codex")) {
166
+ if (this.config.providers.includes("codex") && this.configFor("codex").enabled) {
141
167
  const found = await this.discoverCodexCandidates();
142
168
  candidates.push(...found.candidates);
143
169
  if (found.skipReason) skips.push({ provider: "codex", reason: found.skipReason });
@@ -226,6 +252,9 @@ export class OrchestratorQuotaPoller {
226
252
  if (!await this.ensureLease(candidate, state, now)) return;
227
253
  if (state.nextPollAt !== undefined && state.nextPollAt > now) return;
228
254
 
255
+ // Per-provider cadence (#605): the configured interval governs the gap between
256
+ // successful polls and the post-failure retry once a first attempt has landed.
257
+ const pollIntervalMs = this.configFor(candidate.provider).pollIntervalMs;
229
258
  const lastAttemptAt = now;
230
259
  try {
231
260
  const sample = await this.collect(candidate);
@@ -240,13 +269,17 @@ export class OrchestratorQuotaPoller {
240
269
  };
241
270
  await this.relay.reportProviderQuota(update);
242
271
  state.lastAttemptAt = update.lastAttemptAt;
243
- state.nextPollAt = now + QUOTA_POLL_INTERVAL_MS;
272
+ state.nextPollAt = now + pollIntervalMs;
244
273
  } catch (error) {
245
274
  const retryAfterMs = quotaRetryAfterMs(error);
246
275
  const lastError = providerQuotaErrorFromCollectorError(error, retryAfterMs);
247
- const retryDelayMs = retryAfterMs ?? (state.lastAttemptAt ? QUOTA_POLL_INTERVAL_MS : QUOTA_FAST_RETRY_MS);
276
+ const retryDelayMs = retryAfterMs ?? (state.lastAttemptAt ? pollIntervalMs : QUOTA_FAST_RETRY_MS);
248
277
  state.lastAttemptAt = lastAttemptAt;
249
278
  state.nextPollAt = now + retryDelayMs;
279
+ if (candidate.provider === "claude" && retryAfterMs !== undefined) {
280
+ this.logFailure(candidate, error, retryAfterMs);
281
+ return;
282
+ }
250
283
  await this.relay.reportProviderQuota({
251
284
  provider: candidate.provider,
252
285
  accountKey: candidate.accountKey,
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, ProviderQuotaLeaseAcquireInput, ProviderQuotaLeaseAcquireResult, ProviderQuotaUpdateInput, WorkspaceMetadata, WorkspaceMode, ManagedSessionExitDiagnostics as SdkManagedSessionExitDiagnostics } from "agent-relay-sdk";
5
+ import type { AgentLifecycle, ProviderQuotaConfigMap, 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 {
@@ -14,6 +14,7 @@ export interface RelayClient {
14
14
  acquireProviderQuotaLease(orchestratorId: string, input: ProviderQuotaLeaseAcquireInput): Promise<ProviderQuotaLeaseAcquireResult>;
15
15
  releaseProviderQuotaLease(orchestratorId: string, input: ProviderQuotaLeaseAcquireInput & { leaseToken: string }): Promise<{ released: boolean }>;
16
16
  reportProviderQuota(input: ProviderQuotaUpdateInput): Promise<unknown>;
17
+ getProviderQuotaConfig(): Promise<ProviderQuotaConfigMap>;
17
18
  setApiUrl(url: string): void;
18
19
  startHeartbeatLoop(): void;
19
20
  stopHeartbeatLoop(): void;
@@ -251,6 +252,9 @@ export function createRelayClient(config: OrchestratorConfig, probeCache: Provid
251
252
  reportProviderQuota(input: ProviderQuotaUpdateInput): Promise<unknown> {
252
253
  return http.upsertProviderQuota(input);
253
254
  },
255
+ getProviderQuotaConfig(): Promise<ProviderQuotaConfigMap> {
256
+ return http.getProviderQuotaConfig();
257
+ },
254
258
  get connected() { return connected; },
255
259
  };
256
260
  }