agents-relay 2.0.3 → 2.0.5
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/README.md +1 -1
- package/dist/dashboard-html.js +29 -12
- package/dist/github-runtime-usage.js +7 -4
- package/dist/relayd.js +11 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,7 +160,7 @@ reconcile leases and launches ready tasks without waiting inside the runtime sch
|
|
|
160
160
|
|
|
161
161
|
`agents-relayd` without `--pr`/`--id` runs in workspace mode by default: it discovers nested Git repositories under `~/Workspace`, keeps only GitHub `origin` remotes, and aggregates their managed PR jobs into one dashboard and worker pool. Pass `--workspace PATH` to use another root. `--repo OWNER/REPO` remains the single-repository pool mode.
|
|
162
162
|
|
|
163
|
-
For automation, Agents Relay can use GitHub App installation authentication via `AGENTS_RELAY_GITHUB_APP_ID`, `AGENTS_RELAY_GITHUB_APP_INSTALLATION_ID`, and `AGENTS_RELAY_GITHUB_APP_PRIVATE_KEY_FILE` (or matching CLI flags). Webhook mode is
|
|
163
|
+
For automation, Agents Relay can use GitHub App installation authentication via `AGENTS_RELAY_GITHUB_APP_ID`, `AGENTS_RELAY_GITHUB_APP_INSTALLATION_ID`, and `AGENTS_RELAY_GITHUB_APP_PRIVATE_KEY_FILE` (or matching CLI flags). Webhook mode is local-first: targeted webhook reconciliation is primary, dashboard auto-refresh reads the disposable in-process view restored from SQLite RelayStore state, and the broad watchdog drops to a 30-minute fallback. No separate persisted dashboard Job cache is maintained. See [docs/github-app-rate-limit.md](docs/github-app-rate-limit.md).
|
|
164
164
|
|
|
165
165
|
V1 assumes one active runner/daemon per job. Use a distributed atomic lease before running multiple reconcilers. Do not put secrets, credentials, private prompts, or large private payloads in the PR-body Job marker or PR-comment Task markers; store only summaries and artifact references.
|
|
166
166
|
|
package/dist/dashboard-html.js
CHANGED
|
@@ -397,36 +397,53 @@ function githubBucket(label,bucket){const row=element('div','usage-row');const h
|
|
|
397
397
|
function githubCounterLine(label,values){const entries=Object.entries(values||{}).sort((a,b)=>Number(b[1])-Number(a[1])).slice(0,8);return entries.length?element('div','subtle',text(label+' · '+entries.map(([key,count])=>key+' '+count).join(' · '))):null;}
|
|
398
398
|
function githubObserved(label,observed){
|
|
399
399
|
if(!observed)return null;
|
|
400
|
+
if(Number.isFinite(observed.limit)&&Number.isFinite(observed.remaining)){const row=githubBucket(label,observed);if(observed.reason)row.title=observed.reason;return row;}
|
|
400
401
|
const state=observed.throttled?'Throttled':observed.available===true?'Available':observed.available===false?'Unavailable':'Unknown';
|
|
401
402
|
const parts=[label+' observed',state];
|
|
402
403
|
if(observed.status!==null)parts.push('status '+observed.status);
|
|
403
|
-
if(observed.remaining!==null)parts.push('remaining '+observed.remaining);
|
|
404
|
-
if(observed.headersCaptured&&observed.reset!==null)parts.push(shortReset(observed.reset));
|
|
405
404
|
if(observed.retryAfterSeconds!==null)parts.push('retry '+observed.retryAfterSeconds+'s');
|
|
406
405
|
const row=element('div',observed.throttled?'usage-row error-state':'usage-row subtle');
|
|
407
406
|
row.textContent=parts.filter(Boolean).join(' · ');
|
|
408
407
|
if(observed.reason)row.title=observed.reason;
|
|
409
408
|
return row;
|
|
410
409
|
}
|
|
410
|
+
function githubSecondary(status){
|
|
411
|
+
const rejection=status?.runtimeRateUsage?.rejection;
|
|
412
|
+
const row=element('div','usage-row');
|
|
413
|
+
const heading=element('div','usage-label');
|
|
414
|
+
if(rejection?.kind!=='secondary'){heading.append(element('span','',text('Secondary protection')),element('span','',text('No active cooldown')));row.append(heading);return row;}
|
|
415
|
+
const start=Date.parse(rejection.observedAt);
|
|
416
|
+
const until=Date.parse(rejection.cooldownUntil||status?.cooldownUntil||'');
|
|
417
|
+
if(!Number.isFinite(start)||!Number.isFinite(until)||until<=start){heading.append(element('span','',text('Secondary protection')),element('span','',text('Throttled')));row.classList.add('error-state');row.append(heading);return row;}
|
|
418
|
+
const total=until-start;
|
|
419
|
+
const remaining=Math.max(0,until-Date.now());
|
|
420
|
+
const percent=Math.max(0,Math.min(100,100*remaining/total));
|
|
421
|
+
heading.append(element('span','',text('Secondary cooldown')),element('span','',text(Math.ceil(remaining/1000)+'s')));
|
|
422
|
+
const track=element('div','usage-track');track.setAttribute('role','progressbar');track.setAttribute('aria-label','Secondary cooldown remaining');track.setAttribute('aria-valuemin','0');track.setAttribute('aria-valuemax','100');track.setAttribute('aria-valuenow',String(Math.round(percent)));const fill=element('div','usage-fill');fill.style.width=percent+'%';track.append(fill);
|
|
423
|
+
const hint=element('div','subtle',text('Retry '+new Date(until).toLocaleString()));hint.title=new Date(until).toLocaleString();
|
|
424
|
+
row.append(heading,track,hint);
|
|
425
|
+
return row;
|
|
426
|
+
}
|
|
411
427
|
function githubDiagnostics(status){const body=element('div','usage-details-body');if(status?.calls)body.append(element('div','subtle',text('Calls · GraphQL '+(status.calls.graphql||0)+' · REST '+(status.calls.rest||0)+' · quota '+(status.calls.rateLimit||0))));const operations=githubCounterLine('GraphQL operations',status?.graphql?.operations);if(operations)body.append(operations);const contexts=githubCounterLine('GraphQL contexts',status?.graphql?.contexts);if(contexts)body.append(contexts);if(status?.readPaths){body.append(element('div','subtle',text('Reads · scans '+(status.readPaths.workspaceScans||0)+' · PR '+(status.readPaths.prRefreshes||0)+' · coalesced '+(status.readPaths.prRefreshCoalesced||0))));const triggers=githubCounterLine('Refresh triggers',status.readPaths.prRefreshesByTrigger);if(triggers)body.append(triggers);const coalesced=githubCounterLine('Coalesced',status.readPaths.prRefreshCoalescedByTrigger);if(coalesced)body.append(coalesced);}if(!body.children.length)return null;const details=element('details','usage-details');details.append(element('summary','',text('Diagnostics')),body);return details;}
|
|
412
428
|
function githubUsageCard(snapshot,status){
|
|
413
429
|
const card=element('section','usage-provider');
|
|
414
430
|
card.append(element('h3','',text('GitHub API')));
|
|
415
431
|
const resources=snapshot?.resources||{};
|
|
432
|
+
const observed=status?.runtimeRateUsage?.resources||{};
|
|
416
433
|
const bars=element('div','usage-bars');
|
|
434
|
+
bars.append(githubSecondary(status),element('div','usage-label',text('Request-time X-RateLimit headers')));
|
|
435
|
+
let observedCount=0;
|
|
436
|
+
for(const row of [githubObserved('REST',observed.core),githubObserved('GraphQL',observed.graphql),githubObserved('Search',observed.search)])if(row){bars.append(row);observedCount++;}
|
|
437
|
+
if(!observedCount)bars.append(element('p','empty',text('No request-time headers observed')));
|
|
417
438
|
bars.append(element('div','usage-label',text('Primary quota (/rate_limit)')));
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if(
|
|
421
|
-
if(
|
|
422
|
-
const observed=status?.runtimeRateUsage?.resources||{};
|
|
423
|
-
for(const row of [githubObserved('REST',observed.core),githubObserved('GraphQL',observed.graphql),githubObserved('Search',observed.search)])if(row)bars.append(row);
|
|
424
|
-
if(status?.runtimeRateUsage?.rejection){
|
|
439
|
+
let quotaCount=0;
|
|
440
|
+
for(const pair of [['REST',resources.core],['GraphQL',resources.graphql],['Search',resources.search]])if(pair[1]){bars.append(githubBucket(pair[0],pair[1]));quotaCount++;}
|
|
441
|
+
if(!quotaCount)bars.append(element('p','empty',text('Quota unavailable')));
|
|
442
|
+
if(status?.runtimeRateUsage?.rejection&&status.runtimeRateUsage.rejection.kind!=='secondary'){
|
|
425
443
|
const rejection=status.runtimeRateUsage.rejection;
|
|
426
|
-
|
|
427
|
-
bars.append(element('div','error-state',text('Throttled · '+(rejection.kind==='unknown'?'rate limit':rejection.kind)+' limit'+cooldown)));
|
|
444
|
+
bars.append(element('div','error-state',text('Throttled · '+(rejection.kind==='unknown'?'rate limit':rejection.kind)+' limit')));
|
|
428
445
|
}
|
|
429
|
-
if(status?.limited){
|
|
446
|
+
if(status?.limited&&status?.runtimeRateUsage?.rejection?.kind!=='secondary'){
|
|
430
447
|
const until=status.cooldownUntil?' until '+new Date(status.cooldownUntil).toLocaleTimeString():'';
|
|
431
448
|
bars.append(element('div','error-state',text('Cooldown active'+until)));
|
|
432
449
|
}
|
|
@@ -14,12 +14,13 @@ function transportResource(requestChannel, headers) {
|
|
|
14
14
|
return requestChannel === 'graphql' ? 'graphql' : 'core';
|
|
15
15
|
}
|
|
16
16
|
function rejectionKind(headers, reason) {
|
|
17
|
-
|
|
17
|
+
const remaining = headerNumber(headers['x-ratelimit-remaining']);
|
|
18
|
+
if (remaining === 0)
|
|
19
|
+
return 'primary';
|
|
20
|
+
if (/secondary rate/i.test(reason) || headerNumber(headers['retry-after']) !== null)
|
|
18
21
|
return 'secondary';
|
|
19
|
-
if (/rate limit/i.test(reason) &&
|
|
22
|
+
if (/rate limit/i.test(reason) && remaining !== null && remaining > 0)
|
|
20
23
|
return 'secondary';
|
|
21
|
-
if (/rate limit/i.test(reason))
|
|
22
|
-
return 'primary';
|
|
23
24
|
return 'unknown';
|
|
24
25
|
}
|
|
25
26
|
function isThrottled(status, reason) {
|
|
@@ -51,6 +52,8 @@ export function runtimeRejection(observation) {
|
|
|
51
52
|
const headers = {};
|
|
52
53
|
if (observation.remaining !== null)
|
|
53
54
|
headers['x-ratelimit-remaining'] = String(observation.remaining);
|
|
55
|
+
if (observation.retryAfterSeconds !== null)
|
|
56
|
+
headers['retry-after'] = String(observation.retryAfterSeconds);
|
|
54
57
|
const retryAfterSeconds = observation.retryAfterSeconds;
|
|
55
58
|
return {
|
|
56
59
|
resource: observation.resource,
|
package/dist/relayd.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { once } from 'node:events';
|
|
3
|
-
import {
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { ChatGptAdapter, CodexAdapter } from './adapters.js';
|
|
@@ -134,17 +134,18 @@ export async function runDaemon(argv) {
|
|
|
134
134
|
projectionWriters.set(key, projected.projection);
|
|
135
135
|
return projected;
|
|
136
136
|
};
|
|
137
|
-
const makeReconciler = (store, maxConcurrent) => new Reconciler(store, { owner: `relayd-${process.pid}`, maxConcurrent, leaseMs: Number(value(argv, '--lease-ms', '300000')), adapters: [new CodexAdapter(value(argv, '--codex', 'codex')), new ChatGptAdapter()], continuations: [new CodexThreadContinuation(value(argv, '--codex', 'codex')), new CommandContinuation(), new WebhookContinuation()], planner: runtimePlanner(argv), usageSnapshotProvider: () => usageRegistry.snapshot(), runtimeSummaryProvider: () => ({ chatgptRunning: summarizeRuntime(cachedOverviews.map(item => item.job)).workers.chatgpt }), decisionEngine: new LayaCliDecisionEngine(), decisionMode: decisionMode(), decisionAuditStore, eventBus: bus, reviewer: runtimeReviewer(), projectPathForJob: async (job) => resolveRepositoryProjectPath(repositoryPaths.get(job.repository) ?? await resolveManagedProjectPath(job.repository, explicitProjectPath, workspaceRoot), job.projectPath) });
|
|
138
137
|
if (repositories.length === 0)
|
|
139
138
|
throw new Error(`No Git repositories with GitHub origins found under ${normalized.workspaceRoot ?? defaultWorkspaceRoot()}`);
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
139
|
+
const localSnapshots = await discoverLocalRelayStoreSnapshots(join(homedir(), '.agents-relay', 'stores'));
|
|
140
|
+
const knownRepositories = new Set(repositories);
|
|
141
|
+
cachedOverviews = dashboardManagedJobs(localSnapshots.flatMap(snapshot => {
|
|
142
|
+
if (!snapshot.facts)
|
|
143
|
+
return [];
|
|
144
|
+
return snapshot.jobs.filter(job => knownRepositories.has(job.repository)).map(job => ({
|
|
145
|
+
job, githubState: snapshot.facts.state, draft: false, mergedAt: null, closedAt: null, publication: null,
|
|
146
|
+
}));
|
|
147
|
+
}));
|
|
148
|
+
const makeReconciler = (store, maxConcurrent) => new Reconciler(store, { owner: `relayd-${process.pid}`, maxConcurrent, leaseMs: Number(value(argv, '--lease-ms', '300000')), adapters: [new CodexAdapter(value(argv, '--codex', 'codex')), new ChatGptAdapter()], continuations: [new CodexThreadContinuation(value(argv, '--codex', 'codex')), new CommandContinuation(), new WebhookContinuation()], planner: runtimePlanner(argv), usageSnapshotProvider: () => usageRegistry.snapshot(), runtimeSummaryProvider: () => ({ chatgptRunning: summarizeRuntime(cachedOverviews.map(item => item.job)).workers.chatgpt }), decisionEngine: new LayaCliDecisionEngine(), decisionMode: decisionMode(), decisionAuditStore, eventBus: bus, reviewer: runtimeReviewer(), projectPathForJob: async (job) => resolveRepositoryProjectPath(repositoryPaths.get(job.repository) ?? await resolveManagedProjectPath(job.repository, explicitProjectPath, workspaceRoot), job.projectPath) });
|
|
148
149
|
const refreshAll = async (full = false) => {
|
|
149
150
|
if (client.rateLimitStatus().limited)
|
|
150
151
|
return cachedOverviews;
|
|
@@ -157,11 +158,6 @@ export async function runDaemon(argv) {
|
|
|
157
158
|
const scanned = new Set(scanRepositories);
|
|
158
159
|
const merged = dashboardManagedJobs([...cachedOverviews.filter(item => !scanned.has(item.job.repository)), ...refreshed]);
|
|
159
160
|
cachedOverviews = await overlayOperationalOverviews(merged, (repository, prNumber) => runtimeStore(repository, prNumber));
|
|
160
|
-
try {
|
|
161
|
-
await mkdir(cacheDirectory, { recursive: true });
|
|
162
|
-
await writeFile(cacheFile, JSON.stringify(cachedOverviews), 'utf8');
|
|
163
|
-
}
|
|
164
|
-
catch { /* cache is best effort */ }
|
|
165
161
|
return cachedOverviews;
|
|
166
162
|
}
|
|
167
163
|
catch (error) {
|
|
@@ -175,13 +171,6 @@ export async function runDaemon(argv) {
|
|
|
175
171
|
cachedOverviews = await overlayOperationalOverviews(cachedOverviews, (repository, prNumber) => runtimeStore(repository, prNumber));
|
|
176
172
|
return cachedOverviews;
|
|
177
173
|
};
|
|
178
|
-
const persistCache = async () => {
|
|
179
|
-
try {
|
|
180
|
-
await mkdir(cacheDirectory, { recursive: true });
|
|
181
|
-
await writeFile(cacheFile, JSON.stringify(cachedOverviews), 'utf8');
|
|
182
|
-
}
|
|
183
|
-
catch { /* cache is best effort */ }
|
|
184
|
-
};
|
|
185
174
|
const pullRequestRefreshFlights = new Map();
|
|
186
175
|
const pullRequestRefreshCache = new Map();
|
|
187
176
|
const incrementTrigger = (counts, trigger) => { counts[trigger] = (counts[trigger] ?? 0) + 1; };
|
|
@@ -209,7 +198,6 @@ export async function runDaemon(argv) {
|
|
|
209
198
|
cachedOverviews = cachedOverviews.filter(item => item.job.repository !== repository || item.job.prNumber !== prNumber);
|
|
210
199
|
cachedOverviews.push(...operational);
|
|
211
200
|
cachedOverviews = dashboardManagedJobs(cachedOverviews);
|
|
212
|
-
await persistCache();
|
|
213
201
|
const operationalSnapshot = { ...snapshot, overviews: operational };
|
|
214
202
|
pullRequestRefreshCache.set(key, { snapshot: operationalSnapshot, completedAt: Date.now() });
|
|
215
203
|
return operationalSnapshot;
|
|
@@ -384,21 +372,6 @@ export async function runDaemon(argv) {
|
|
|
384
372
|
store.close(); server.close(() => process.exit(0)); }); };
|
|
385
373
|
process.once('SIGINT', stop);
|
|
386
374
|
process.once('SIGTERM', stop);
|
|
387
|
-
const localSnapshots = await discoverLocalRelayStoreSnapshots(join(homedir(), '.agents-relay', 'stores'));
|
|
388
|
-
const knownRepositories = new Set(repositories);
|
|
389
|
-
const localOverviews = localSnapshots.flatMap(snapshot => {
|
|
390
|
-
if (!snapshot.facts)
|
|
391
|
-
return [];
|
|
392
|
-
return snapshot.jobs.filter(job => knownRepositories.has(job.repository)).map(job => ({
|
|
393
|
-
job, githubState: snapshot.facts.state, draft: false, mergedAt: null, closedAt: null, publication: null,
|
|
394
|
-
}));
|
|
395
|
-
});
|
|
396
|
-
const byJob = new Map();
|
|
397
|
-
for (const item of cachedOverviews)
|
|
398
|
-
byJob.set(item.job.repository + '#' + item.job.prNumber + ':' + item.job.id, item);
|
|
399
|
-
for (const item of localOverviews)
|
|
400
|
-
byJob.set(item.job.repository + '#' + item.job.prNumber + ':' + item.job.id, item);
|
|
401
|
-
cachedOverviews = dashboardManagedJobs([...byJob.values()]);
|
|
402
375
|
await pool.reconcileKnown(cachedOverviews);
|
|
403
376
|
await scheduleHeartbeatSafely();
|
|
404
377
|
await flushProjectionsSafely();
|