agents-relay 1.0.139 → 1.0.141
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/dashboard-html.js +1 -1
- package/dist/dashboard.js +1 -1
- package/dist/reconciler-runtime.js +11 -0
- package/dist/relayd.js +26 -9
- package/package.json +1 -1
package/dist/dashboard-html.js
CHANGED
|
@@ -373,7 +373,7 @@ function formatCredit(value,currency){if(value===null||value===undefined||!Numbe
|
|
|
373
373
|
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;}
|
|
374
374
|
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;}
|
|
375
375
|
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;}
|
|
376
|
-
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))));const operations=githubCounterLine('GraphQL operations',status?.graphql?.operations);if(operations)bars.append(operations);const contexts=githubCounterLine('GraphQL contexts',status?.graphql?.contexts);if(contexts)bars.append(contexts);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;}
|
|
376
|
+
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))));const operations=githubCounterLine('GraphQL operations',status?.graphql?.operations);if(operations)bars.append(operations);const contexts=githubCounterLine('GraphQL contexts',status?.graphql?.contexts);if(contexts)bars.append(contexts);if(status?.readPaths){bars.append(element('div','subtle',text('Read paths · workspace scans '+(status.readPaths.workspaceScans||0)+' · PR refreshes '+(status.readPaths.prRefreshes||0)+' · coalesced '+(status.readPaths.prRefreshCoalesced||0))));const triggers=githubCounterLine('PR refresh triggers',status.readPaths.prRefreshesByTrigger);if(triggers)bars.append(triggers);const coalesced=githubCounterLine('Coalesced refresh triggers',status.readPaths.prRefreshCoalescedByTrigger);if(coalesced)bars.append(coalesced);}card.append(bars);return card;}
|
|
377
377
|
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||overview.githubStatus)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)));}}
|
|
378
378
|
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;}}
|
|
379
379
|
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/dashboard.js
CHANGED
|
@@ -68,7 +68,7 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
68
68
|
const discoveredJobs = async (forceRefresh = false) => {
|
|
69
69
|
try {
|
|
70
70
|
const jobs = overviewProvider
|
|
71
|
-
? (await overviewProvider(forceRefresh)).map(item => ({ ...item.job, githubState: item.githubState, runtimeStatus: deriveJobRuntimeStatus(item.job, item.githubState), mergedAt: item.mergedAt, closedAt: item.closedAt, draft: item.draft, publication: item.publication }))
|
|
71
|
+
? (await overviewProvider(forceRefresh, forceRefresh ? 'dashboard.jobs-refresh' : 'internal.unknown')).map(item => ({ ...item.job, githubState: item.githubState, runtimeStatus: deriveJobRuntimeStatus(item.job, item.githubState), mergedAt: item.mergedAt, closedAt: item.closedAt, draft: item.draft, publication: item.publication }))
|
|
72
72
|
: jobId ? [await store.load(jobId)] : [];
|
|
73
73
|
lastKnownJobs = jobs;
|
|
74
74
|
return jobs;
|
|
@@ -153,6 +153,15 @@ export class ReconcilerRuntimeBase {
|
|
|
153
153
|
registerExecutionOwner(jobId, taskId, executionId) {
|
|
154
154
|
activeExecutionOwners.set(executionEventSource(jobId, taskId, executionId), this);
|
|
155
155
|
}
|
|
156
|
+
clearTerminalTimer(taskId, executionId) {
|
|
157
|
+
const live = this.live.get(taskId);
|
|
158
|
+
if (!live || live.executionId !== executionId)
|
|
159
|
+
return;
|
|
160
|
+
const timer = this.terminalTimers.get(taskId);
|
|
161
|
+
if (timer)
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
this.terminalTimers.delete(taskId);
|
|
164
|
+
}
|
|
156
165
|
clearExecution(taskId, executionId) {
|
|
157
166
|
const live = this.live.get(taskId);
|
|
158
167
|
if (!executionId || live?.executionId === executionId) {
|
|
@@ -243,6 +252,8 @@ export class ReconcilerRuntimeBase {
|
|
|
243
252
|
const task = job.tasks.find(item => item.id === taskId);
|
|
244
253
|
if (!task || task.executionId !== executionId)
|
|
245
254
|
return;
|
|
255
|
+
if (task.adapter !== 'chatgpt')
|
|
256
|
+
this.clearTerminalTimer(taskId, executionId);
|
|
246
257
|
await this.emit(eventFor(jobId, taskId, task.parentTaskId, task.adapter === 'chatgpt' ? 'task.process.async_exited' : 'task.process.exited', error ? 'failed' : 'succeeded', error ?? result?.summary ?? 'Worker runtime exited', 'orchestrator', { authoritative: false, waitingForTerminalEvent: true, processError: error }, { agent: 'relay', component: 'agents-relay' }));
|
|
247
258
|
}
|
|
248
259
|
async timeoutTask(jobId, taskId, executionId) {
|
package/dist/relayd.js
CHANGED
|
@@ -19,6 +19,10 @@ import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
|
19
19
|
import { decisionMode, LayaCliDecisionEngine } from './decision.js';
|
|
20
20
|
import { JsonlDecisionAuditStore } from './decision-audit.js';
|
|
21
21
|
import { summarizeRuntime } from './runtime-summary.js';
|
|
22
|
+
const PR_REFRESH_COALESCE_MS = 1000;
|
|
23
|
+
export function shouldCoalescePullRequestRefresh(completedAt, now = Date.now(), force = false) {
|
|
24
|
+
return !force && completedAt !== null && now >= completedAt && now - completedAt < PR_REFRESH_COALESCE_MS;
|
|
25
|
+
}
|
|
22
26
|
export const daemonHelp = `Agents Relay daemon runs the dashboard and worker pool.
|
|
23
27
|
|
|
24
28
|
Usage:
|
|
@@ -97,7 +101,7 @@ export async function runDaemon(argv) {
|
|
|
97
101
|
const usageRegistry = codexAndZaiUsageRegistry();
|
|
98
102
|
const decisionAuditStore = new JsonlDecisionAuditStore(process.env.AGENTS_RELAY_DECISION_AUDIT_PATH ?? join(homedir(), '.agents-relay', 'decision-audit.jsonl'));
|
|
99
103
|
let cachedOverviews = [];
|
|
100
|
-
const githubReadPaths = { workspaceScans: 0, prRefreshes: 0 };
|
|
104
|
+
const githubReadPaths = { workspaceScans: 0, prRefreshes: 0, prRefreshesByTrigger: {}, prRefreshCoalesced: 0, prRefreshCoalescedByTrigger: {} };
|
|
101
105
|
let dormantProbeIndex = 0;
|
|
102
106
|
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) });
|
|
103
107
|
if (repositories.length === 0)
|
|
@@ -134,7 +138,7 @@ export async function runDaemon(argv) {
|
|
|
134
138
|
return cachedOverviews;
|
|
135
139
|
}
|
|
136
140
|
};
|
|
137
|
-
const overviews = async (forceRefresh = false) => forceRefresh ? refreshAll(true) : cachedOverviews;
|
|
141
|
+
const overviews = async (forceRefresh = false, _trigger = 'internal.unknown') => forceRefresh ? refreshAll(true) : cachedOverviews;
|
|
138
142
|
const persistCache = async () => {
|
|
139
143
|
try {
|
|
140
144
|
await mkdir(cacheDirectory, { recursive: true });
|
|
@@ -143,20 +147,33 @@ export async function runDaemon(argv) {
|
|
|
143
147
|
catch { /* cache is best effort */ }
|
|
144
148
|
};
|
|
145
149
|
const pullRequestRefreshFlights = new Map();
|
|
146
|
-
const
|
|
150
|
+
const pullRequestRefreshCache = new Map();
|
|
151
|
+
const incrementTrigger = (counts, trigger) => { counts[trigger] = (counts[trigger] ?? 0) + 1; };
|
|
152
|
+
const refreshPullRequestSnapshot = async (repository, prNumber, trigger = 'internal', options = {}) => {
|
|
147
153
|
if (client.rateLimitStatus().limited)
|
|
148
154
|
return { overviews: cachedOverviews.filter(item => item.job.repository === repository && item.job.prNumber === prNumber), seed: { prNode: {}, comments: null } };
|
|
149
155
|
const key = `${repository}#${prNumber}`;
|
|
156
|
+
incrementTrigger(githubReadPaths.prRefreshesByTrigger, trigger);
|
|
150
157
|
const existing = pullRequestRefreshFlights.get(key);
|
|
151
|
-
if (existing)
|
|
158
|
+
if (existing) {
|
|
159
|
+
githubReadPaths.prRefreshCoalesced += 1;
|
|
160
|
+
incrementTrigger(githubReadPaths.prRefreshCoalescedByTrigger, trigger);
|
|
152
161
|
return existing;
|
|
162
|
+
}
|
|
163
|
+
const cached = pullRequestRefreshCache.get(key);
|
|
164
|
+
if (cached && shouldCoalescePullRequestRefresh(cached.completedAt, Date.now(), options.force === true)) {
|
|
165
|
+
githubReadPaths.prRefreshCoalesced += 1;
|
|
166
|
+
incrementTrigger(githubReadPaths.prRefreshCoalescedByTrigger, trigger);
|
|
167
|
+
return cached.snapshot;
|
|
168
|
+
}
|
|
153
169
|
const flight = (async () => {
|
|
154
170
|
githubReadPaths.prRefreshes += 1;
|
|
155
|
-
const snapshot = await withGitHubRequestContext(client,
|
|
171
|
+
const snapshot = await withGitHubRequestContext(client, `relayd.pr-refresh:${trigger}`, () => loadManagedGitHubPullRequestSnapshotGraphql(client, repository, prNumber, trusted));
|
|
156
172
|
cachedOverviews = cachedOverviews.filter(item => item.job.repository !== repository || item.job.prNumber !== prNumber);
|
|
157
173
|
cachedOverviews.push(...snapshot.overviews);
|
|
158
174
|
cachedOverviews = dashboardManagedJobs(cachedOverviews);
|
|
159
175
|
await persistCache();
|
|
176
|
+
pullRequestRefreshCache.set(key, { snapshot, completedAt: Date.now() });
|
|
160
177
|
return snapshot;
|
|
161
178
|
})();
|
|
162
179
|
pullRequestRefreshFlights.set(key, flight);
|
|
@@ -168,7 +185,7 @@ export async function runDaemon(argv) {
|
|
|
168
185
|
pullRequestRefreshFlights.delete(key);
|
|
169
186
|
}
|
|
170
187
|
};
|
|
171
|
-
const refreshPullRequest = async (repository, prNumber) => (await refreshPullRequestSnapshot(repository, prNumber)).overviews;
|
|
188
|
+
const refreshPullRequest = async (repository, prNumber, trigger, options) => (await refreshPullRequestSnapshot(repository, prNumber, trigger, options)).overviews;
|
|
172
189
|
const initial = cachedOverviews;
|
|
173
190
|
const fallback = initial[0]?.job;
|
|
174
191
|
const dashboardStore = fallback ? new GitHubStore(client, fallback.repository, fallback.prNumber, trusted) : new InMemoryStore();
|
|
@@ -196,7 +213,7 @@ export async function runDaemon(argv) {
|
|
|
196
213
|
throw new Error(`Managed job ${jobId} not found`);
|
|
197
214
|
const store = new GitHubStore(client, overview.job.repository, overview.job.prNumber, trusted);
|
|
198
215
|
const result = await dashboardTaskSetState(store)(jobId, taskId, state);
|
|
199
|
-
await refreshPullRequest(overview.job.repository, overview.job.prNumber);
|
|
216
|
+
await refreshPullRequest(overview.job.repository, overview.job.prNumber, 'dashboard.task-state', { force: true });
|
|
200
217
|
return result;
|
|
201
218
|
};
|
|
202
219
|
const secretFile = value(argv, '--github-webhook-secret-file');
|
|
@@ -207,7 +224,7 @@ export async function runDaemon(argv) {
|
|
|
207
224
|
refresh: async (event) => {
|
|
208
225
|
if (!event.repository || !event.pullRequest)
|
|
209
226
|
return [];
|
|
210
|
-
const snapshot = await refreshPullRequestSnapshot(event.repository, event.pullRequest);
|
|
227
|
+
const snapshot = await refreshPullRequestSnapshot(event.repository, event.pullRequest, `github.webhook:${event.event}${event.action ? ':' + event.action : ''}`);
|
|
211
228
|
for (const item of snapshot.overviews)
|
|
212
229
|
pool.seedTargetSnapshot(event.repository, event.pullRequest, item.job.id, snapshot.seed);
|
|
213
230
|
return snapshot.overviews.map(item => item.job.id);
|
|
@@ -233,7 +250,7 @@ export async function runDaemon(argv) {
|
|
|
233
250
|
if (client.rateLimitStatus().limited)
|
|
234
251
|
return cachedJob();
|
|
235
252
|
try {
|
|
236
|
-
const refreshed = (await refreshPullRequest(cached.job.repository, cached.job.prNumber)).find(item => item.job.id === jobId);
|
|
253
|
+
const refreshed = (await refreshPullRequest(cached.job.repository, cached.job.prNumber, 'dashboard.selected-job', { force: true })).find(item => item.job.id === jobId);
|
|
237
254
|
if (!refreshed)
|
|
238
255
|
return null;
|
|
239
256
|
return { ...refreshed.job, githubState: refreshed.githubState, runtimeStatus: deriveJobRuntimeStatus(refreshed.job, refreshed.githubState), mergedAt: refreshed.mergedAt, closedAt: refreshed.closedAt, draft: refreshed.draft, publication: refreshed.publication };
|