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.
@@ -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 refreshPullRequestSnapshot = async (repository, prNumber) => {
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, 'relayd.pr-refresh', () => loadManagedGitHubPullRequestSnapshotGraphql(client, repository, prNumber, trusted));
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.139",
3
+ "version": "1.0.141",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {