agents-relay 1.0.128 → 1.0.130

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.
@@ -368,7 +368,7 @@ function usageProvider(provider){const card=element('section','usage-provider');
368
368
  function formatCredit(value,currency){if(value===null||value===undefined||!Number.isFinite(Number(value)))return '—';const number=Number(value);if(currency==='USD')return '$'+number.toFixed(2);if(currency==='CNY')return '¥'+number.toFixed(2);return number.toFixed(2)+' '+currency;}
369
369
  function creditProvider(provider){const row=element('section','usage-provider');row.append(element('h3','',text(provider.label)));const credits=provider.credits||{};if(!credits.available||!Array.isArray(credits.balances)||!credits.balances.length){row.append(element('p','empty',text(credits.reason||'Credit balance is unavailable.')));return row;}const balance=credits.balances[0];row.append(element('div','detail-value',text(formatCredit(balance.remaining,balance.currency))));return row;}
370
370
  function githubBucket(label,bucket){const row=element('div','usage-row');const heading=element('div','usage-label');heading.append(element('span','',text(label)),element('span','',text(bucket.remaining+' remaining')));row.append(heading);row.append(element('div','subtle',text(bucket.used+' used / '+bucket.limit+' limit')));const reset=resetText({resetsAt:bucket.reset});if(reset)row.append(element('div','subtle',text(reset)));return row;}
371
- function githubUsageCard(snapshot,status){const card=element('section','usage-provider');card.append(element('h3','',text('GitHub API')));const resources=snapshot?.resources||{};const bars=element('div','usage-bars');if(resources.core)bars.append(githubBucket('REST core',resources.core));if(resources.graphql)bars.append(githubBucket('GraphQL',resources.graphql));if(resources.search)bars.append(githubBucket('Search',resources.search));if(!bars.children.length)bars.append(element('p','empty',text('GitHub quota telemetry is unavailable.')));if(status?.limited)bars.append(element('div','error-state',text('Relay cooldown active'+(status.cooldownUntil?' until '+new Date(status.cooldownUntil).toLocaleString():'')));else bars.append(element('div','subtle',text('Relay cooldown inactive')));if(status?.calls)bars.append(element('div','subtle',text('Calls since start · GraphQL '+(status.calls.graphql||0)+' · REST '+(status.calls.rest||0)+' · quota '+(status.calls.rateLimit||0))));if(status?.readPaths)bars.append(element('div','subtle',text('Read paths · workspace scans '+(status.readPaths.workspaceScans||0)+' · PR refreshes '+(status.readPaths.prRefreshes||0))));card.append(bars);return card;}
371
+ function githubUsageCard(snapshot,status){const card=element('section','usage-provider');card.append(element('h3','',text('GitHub API')));const resources=snapshot?.resources||{};const bars=element('div','usage-bars');if(resources.core)bars.append(githubBucket('REST core',resources.core));if(resources.graphql)bars.append(githubBucket('GraphQL',resources.graphql));if(resources.search)bars.append(githubBucket('Search',resources.search));if(!bars.children.length)bars.append(element('p','empty',text('GitHub quota telemetry is unavailable.')));if(status?.limited)bars.append(element('div','error-state',text('Relay cooldown active'+(status.cooldownUntil?' until '+new Date(status.cooldownUntil).toLocaleString():''))));else bars.append(element('div','subtle',text('Relay cooldown inactive')));if(status?.calls)bars.append(element('div','subtle',text('Calls since start · GraphQL '+(status.calls.graphql||0)+' · REST '+(status.calls.rest||0)+' · quota '+(status.calls.rateLimit||0))));if(status?.readPaths)bars.append(element('div','subtle',text('Read paths · workspace scans '+(status.readPaths.workspaceScans||0)+' · PR refreshes '+(status.readPaths.prRefreshes||0))));card.append(bars);return card;}
372
372
  async function drawUsage(){try{const overview=await loadJSON('/api/usage');usageRoot.replaceChildren();usageMeta.textContent='Real account telemetry when available';const providers=Array.isArray(overview.providers)?overview.providers:[];const credits=Array.isArray(overview.credits)?overview.credits:[];const grid=element('div','usage-providers');if(overview.github)grid.append(githubUsageCard(overview.github,overview.githubStatus));for(const provider of providers)grid.append(usageProvider(provider));for(const provider of credits)grid.append(creditProvider(provider));if(!grid.children.length){usageRoot.append(element('p','empty',text('Usage telemetry is unavailable.')));return;}usageRoot.append(grid);}catch(error){usageMeta.textContent='Unavailable';usageRoot.replaceChildren(element('p','error-state',text('Usage unavailable: '+error.message)));}}
373
373
  async function drawRules(){rulesStatus.textContent='';try{const rules=await loadJSON('/api/model-router-rules');rulesText.value=typeof rules.text==='string'?rules.text:'';rulesMeta.textContent=rules.hash&&rules.hash!=='none'?'Loaded · '+rules.hash.slice(0,12):'No custom rules saved';}catch(error){rulesMeta.textContent='Unavailable';rulesStatus.textContent='Could not load rules: '+error.message;}}
374
374
  async function saveRules(){rulesSave.disabled=true;rulesStatus.textContent='Saving…';try{const response=await fetch('/api/model-router-rules',{method:'PUT',headers:{'content-type':'text/plain; charset=utf-8'},body:rulesText.value});if(!response.ok)throw new Error(response.status+' '+await response.text());const rules=await response.json();rulesMeta.textContent='Saved · '+rules.hash.slice(0,12);rulesStatus.textContent='Saved. New routing decisions will load these rules.';}catch(error){rulesStatus.textContent='Save failed: '+error.message;}finally{rulesSave.disabled=false;}}
package/dist/usage.js CHANGED
@@ -4,6 +4,67 @@ import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { createInterface } from 'node:readline';
6
6
  import { spawn } from 'node:child_process';
7
+ export class BrowserWorkspaceUsageReader {
8
+ command;
9
+ timeoutMs;
10
+ now;
11
+ staleAfterMs;
12
+ constructor(command = 'browser-harness', timeoutMs = 10_000, now = () => new Date(), staleAfterMs = 15 * 60_000) {
13
+ this.command = command;
14
+ this.timeoutMs = timeoutMs;
15
+ this.now = now;
16
+ this.staleAfterMs = staleAfterMs;
17
+ }
18
+ async read() {
19
+ const observedAt = this.now().toISOString();
20
+ return await new Promise(resolve => {
21
+ const child = spawn(this.command, [], { stdio: ['pipe', 'pipe', 'ignore'] });
22
+ let output = '';
23
+ let settled = false;
24
+ const finish = (capacity) => {
25
+ if (settled)
26
+ return;
27
+ settled = true;
28
+ clearTimeout(timer);
29
+ child.kill();
30
+ resolve(capacity);
31
+ };
32
+ const unavailable = (reason) => ({ name: 'Harness', poolSize: null, idle: null, leased: null, available: null, observedAt, reason });
33
+ const timer = setTimeout(() => finish(unavailable('Browser Workspace capacity probe timed out.')), this.timeoutMs);
34
+ child.stdout.setEncoding('utf8');
35
+ child.stdout.on('data', chunk => { output += chunk; });
36
+ child.once('error', () => finish(unavailable('Browser Workspace capacity probe could not be started.')));
37
+ child.once('close', code => {
38
+ if (settled)
39
+ return;
40
+ const lines = output.trim().split('\n').reverse();
41
+ for (const line of lines) {
42
+ try {
43
+ const raw = JSON.parse(line);
44
+ const name = typeof raw.name === 'string' ? raw.name : 'Harness';
45
+ const rawObservedAt = typeof raw.observedAt === 'string' && !Number.isNaN(Date.parse(raw.observedAt)) ? raw.observedAt : observedAt;
46
+ if (Date.parse(observedAt) - Date.parse(rawObservedAt) > this.staleAfterMs) {
47
+ finish(unavailable('Browser Workspace capacity probe is stale.'));
48
+ return;
49
+ }
50
+ const numberOrNull = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
51
+ const idle = numberOrNull(raw.idle);
52
+ const leased = numberOrNull(raw.leased);
53
+ const poolSize = numberOrNull(raw.poolSize);
54
+ const available = typeof raw.available === 'boolean' ? raw.available : idle === null ? null : idle > 0;
55
+ if (idle !== null || leased !== null || poolSize !== null || available !== null) {
56
+ finish({ name, poolSize, idle, leased, available, observedAt: rawObservedAt, reason: null });
57
+ return;
58
+ }
59
+ }
60
+ catch { /* browser-harness may print startup diagnostics before JSON */ }
61
+ }
62
+ finish(unavailable(code === 0 ? 'Browser Workspace capacity probe returned no usable capacity.' : `Browser Workspace capacity probe exited with code ${code ?? 'unknown'}.`));
63
+ });
64
+ child.stdin.end('import json\nprint(json.dumps(workspace_capacity()))\n');
65
+ });
66
+ }
67
+ }
7
68
  function finiteNumber(value) {
8
69
  if (typeof value === 'number' && Number.isFinite(value))
9
70
  return value;
@@ -320,18 +381,21 @@ export class UsageRegistry {
320
381
  zaiReader;
321
382
  deepSeekReader;
322
383
  openRouterReader;
323
- constructor(openaiReader, zaiReader = new ZaiUsageReader(), deepSeekReader = null, openRouterReader = null) {
384
+ browserWorkspaceReader;
385
+ constructor(openaiReader, zaiReader = new ZaiUsageReader(), deepSeekReader = null, openRouterReader = null, browserWorkspaceReader = null) {
324
386
  this.openaiReader = openaiReader;
325
387
  this.zaiReader = zaiReader;
326
388
  this.deepSeekReader = deepSeekReader;
327
389
  this.openRouterReader = openRouterReader;
390
+ this.browserWorkspaceReader = browserWorkspaceReader;
328
391
  }
329
392
  async snapshot() {
330
- const [openai, zai, deepseek, openrouter] = await Promise.all([
393
+ const [openai, zai, deepseek, openrouter, browserWorkspace] = await Promise.all([
331
394
  this.openaiReader.read(),
332
395
  this.zaiReader.read(),
333
396
  this.deepSeekReader?.read() ?? Promise.resolve(null),
334
397
  this.openRouterReader?.read() ?? Promise.resolve(null),
398
+ this.browserWorkspaceReader?.read() ?? Promise.resolve(null),
335
399
  ]);
336
400
  const credits = [];
337
401
  if (deepseek)
@@ -345,6 +409,7 @@ export class UsageRegistry {
345
409
  { id: 'zai', label: 'Z.ai', usage: zai },
346
410
  ],
347
411
  credits,
412
+ ...(browserWorkspace ? { browserWorkspace } : {}),
348
413
  };
349
414
  }
350
415
  }
@@ -370,5 +435,5 @@ export class UnavailableUsageReader {
370
435
  }
371
436
  }
372
437
  export function codexAndZaiUsageRegistry(codexHome, _legacyWindowDays) {
373
- return new UsageRegistry(new CodexApiUsageReader(), new ZaiUsageReader(), process.env.DEEPSEEK_API_KEY ? new DeepSeekCreditReader() : null, process.env.OPENROUTER_API_KEY ? new OpenRouterCreditReader() : null);
438
+ return new UsageRegistry(new CodexApiUsageReader(), new ZaiUsageReader(), process.env.DEEPSEEK_API_KEY ? new DeepSeekCreditReader() : null, process.env.OPENROUTER_API_KEY ? new OpenRouterCreditReader() : null, new BrowserWorkspaceUsageReader());
374
439
  }
@@ -68,6 +68,7 @@ export function workerCandidates(adapters, registered, snapshot) {
68
68
  resourceResetAt: resource.resetAt,
69
69
  resourceResetInMinutes: resource.resetInMinutes,
70
70
  resourceWindows: resource.windows,
71
+ browserWorkspaceCapacity: adapter.name === 'chatgpt' ? snapshot?.browserWorkspace : undefined,
71
72
  };
72
73
  });
73
74
  });
@@ -82,6 +83,8 @@ export function satisfiesWorker(candidate, requirements) {
82
83
  return false;
83
84
  if (requirements.runtime && candidate.runtime !== requirements.runtime)
84
85
  return false;
86
+ if (candidate.adapter === 'chatgpt' && candidate.browserWorkspaceCapacity?.idle === 0)
87
+ return false;
85
88
  return true;
86
89
  }
87
90
  export function eligibleWorkerCandidates(candidates, requirements) {
@@ -104,6 +107,7 @@ function selection(selected, selectedBy, decisionEvidence) {
104
107
  resourceResetAt: selected.resourceResetAt,
105
108
  resourceResetInMinutes: selected.resourceResetInMinutes,
106
109
  resourceWindows: selected.resourceWindows,
110
+ browserWorkspaceCapacity: selected.browserWorkspaceCapacity,
107
111
  decisionEvidence,
108
112
  };
109
113
  }
@@ -111,15 +115,21 @@ function selection(selected, selectedBy, decisionEvidence) {
111
115
  export function selectWorker(candidates, requirements) {
112
116
  const eligible = eligibleWorkerCandidates(candidates, requirements);
113
117
  if (eligible.length === 0)
114
- throw new Error('No eligible worker satisfies the authoritative worker constraints');
118
+ throw new Error(noEligibleWorkerReason(candidates));
115
119
  const selected = deterministicWorker(eligible);
116
120
  return { candidate: selected, selection: selection(selected, 'worker-router:fallback') };
117
121
  }
122
+ function noEligibleWorkerReason(candidates) {
123
+ if (candidates.some(candidate => candidate.adapter === 'chatgpt' && candidate.browserWorkspaceCapacity?.idle === 0)) {
124
+ return 'No eligible worker: ChatGPT Browser Workspace has no idle capacity.';
125
+ }
126
+ return 'No eligible worker satisfies the authoritative worker constraints';
127
+ }
118
128
  /** Gather factual worker/task/resource context, then let decide() choose among legal workers. */
119
129
  export async function routeWorker(candidates, requirements, options = {}) {
120
130
  const eligible = eligibleWorkerCandidates(candidates, requirements);
121
131
  if (eligible.length === 0)
122
- throw new Error('No eligible worker satisfies the authoritative worker constraints');
132
+ throw new Error(noEligibleWorkerReason(candidates));
123
133
  const preferredChatgptConcurrency = options.preferredChatgptConcurrency ?? DEFAULT_CHATGPT_PREFERRED_CONCURRENCY;
124
134
  const chatgptRunning = options.chatgptRunning ?? 0;
125
135
  const deterministic = deterministicWorker(eligible);
@@ -152,6 +162,7 @@ export async function routeWorker(candidates, requirements, options = {}) {
152
162
  resourceResetAt: item.resourceResetAt ?? null,
153
163
  resourceResetInMinutes: item.resourceResetInMinutes ?? null,
154
164
  resourceWindows: item.resourceWindows ?? null,
165
+ browserWorkspaceCapacity: item.browserWorkspaceCapacity ?? null,
155
166
  })),
156
167
  }));
157
168
  const choice = await chooseWithDecision(options.decisionEngine, options.decisionMode ?? 'off', { facts }, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.128",
3
+ "version": "1.0.130",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {