agents-relay 2.0.2 → 2.0.4

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.
@@ -2,6 +2,7 @@ import { applyJobGraphTarget, applyTaskGraphTarget } from './workflow/runtime.js
2
2
  import { jobLifecycleV2, taskLifecycleV2 } from './workflow/definitions-v2.js';
3
3
  import { dispatchWorkflowTargetOverride } from './workflow/dispatcher.js';
4
4
  import { dispatchTaskAction } from './task-actions.js';
5
+ import { applyJobCancelAction } from './job-actions.js';
5
6
  export function dashboardTaskAction(store, reconciler, action) {
6
7
  return async (jobId, taskId) => {
7
8
  const job = await store.load(jobId);
@@ -20,6 +21,12 @@ export function dashboardTaskAction(store, reconciler, action) {
20
21
  export function dashboardJobSetState(store) {
21
22
  return async (jobId, state) => {
22
23
  const job = await store.load(jobId);
24
+ if (state === 'CANCELLED') {
25
+ if (job.state !== 'CANCELLED')
26
+ await applyJobCancelAction(store, job);
27
+ await store.closePullRequest?.();
28
+ return await store.load(job.id);
29
+ }
23
30
  if (job.state === state)
24
31
  return job;
25
32
  await dispatchWorkflowTargetOverride(jobLifecycleV2, {
@@ -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
- if(resources.core)bars.append(githubBucket('REST',resources.core));
419
- if(resources.graphql)bars.append(githubBucket('GraphQL',resources.graphql));
420
- if(resources.search)bars.append(githubBucket('Search',resources.search));
421
- if(!bars.children.length)bars.append(element('p','empty',text('Quota unavailable')));
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
- const cooldown=rejection.cooldownUntil?' · cooldown '+new Date(rejection.cooldownUntil).toLocaleTimeString():'';
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
- if (/secondary rate|graphql_rate_limit/i.test(reason))
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) && headerNumber(headers['x-ratelimit-remaining']) !== 0)
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {