agents-relay 1.0.129 → 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.
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.129",
3
+ "version": "1.0.130",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {