agents-relay 2.0.78 → 2.0.79

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.
@@ -352,7 +352,7 @@ function parseAgentGraphSource(source){if(typeof source!=='string'||!source.trim
352
352
  function agentGraphField(task){if(typeof task.agentGraph!=='string'||!task.agentGraph.trim())return null;const box=element('div','detail-group');box.style.gridColumn='1 / -1';box.append(element('p','detail-label',text('Task agent graph')));const graph=element('div','agent-graph');const parsed=parseAgentGraphSource(task.agentGraph);if(parsed&&parsed.waves.length){parsed.waves.forEach((wave,index)=>{const row=element('div','agent-wave');if(index>0)row.append(element('span','agent-arrow',text('↓')));for(const id of wave)row.append(element('span','agent-node',text(id+' · '+(parsed.agents.get(id)||id))));graph.append(row);});}const source=document.createElement('pre');source.className='agent-graph-source';source.textContent=task.agentGraph;graph.append(source);box.append(graph);return box;}
353
353
  function clearEvents(){displayedEventIds.clear();eventFeed.replaceChildren(element('li','empty',text('Waiting for events.')));}
354
354
  async function loadEventHistory(){const job=selected();if(!job){clearEvents();return;}const params=new URLSearchParams({job:job.id,limit:String(MAX_EVENTS)});if(eventTaskId)params.set('task',eventTaskId);try{const history=await loadJSON('/api/events?'+params.toString());clearEvents();for(const event of Array.isArray(history)?history.slice().reverse():[])addEvent(event);}catch{clearEvents();}}
355
- function copySelectedJobIdentity(job){if(!job||!navigator.clipboard?.writeText)return;void navigator.clipboard.writeText(repoName(job.repository)+'/pr#'+job.prNumber).catch(()=>{});}
355
+ function copySelectedJobIdentity(job){if(!job||!navigator.clipboard?.writeText)return;void navigator.clipboard.writeText(jobChatRef(job)).catch(()=>{});}
356
356
  function selectJob(job){selectedJob=jobSelectionKey(job);selectedTaskId=null;eventTaskId=null;rememberSelection();clearEvents();location.hash=selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs';copySelectedJobIdentity(job);renderAll();void loadEventHistory();}
357
357
  function matchesQuickFilter(job){if(quickMode==='autonomous')return job.executionMode==='autonomous';if(quickMode==='draft')return job.draft||job.state==='DRAFT';if(quickMode==='status')return !!selectedRuntimeStatus&&(job.runtimeStatus||'IDLE')===selectedRuntimeStatus;return true;}
358
358
  function matchesSearch(job){const query=searchQuery.trim().toLowerCase();if(!query)return true;const pr=String(job.prNumber||'');const normalized=query.replace(/^pr\s*#?/,'').replace(/^#/,'');return String(job.title||job.id||'').toLowerCase().includes(query)||pr.includes(normalized);}
@@ -384,7 +384,7 @@ const DEFAULT_CHAT_SHORTCUTS=[
384
384
  ].join('\\n');
385
385
  function chatShortcutText(){return localStorage.getItem(CHAT_SHORTCUTS_KEY)||DEFAULT_CHAT_SHORTCUTS;}
386
386
  function parseChatShortcuts(source){return String(source||'').split(/\\r?\\n/).map(line=>line.trim()).filter(Boolean).map(line=>{const split=line.indexOf(':');return split>0?{name:line.slice(0,split).trim(),prompt:line.slice(split+1).trim()}:null;}).filter(item=>item&&item.name&&item.prompt);}
387
- function jobChatRef(job){return repoName(job?.repository)+'/pr#'+value(job?.prNumber);}
387
+ function jobChatRef(job){const pr=Number(job?.externalPrNumber||job?.prNumber||0);return repoName(job?.repository)+"/job:"+value(job?.id)+(pr>0?"/pr#"+pr:"")+": "+value(job?.title||job?.id);}
388
388
  function chatShortcutUrl(prompt){return 'https://chatgpt.com/?prompt='+encodeURIComponent(prompt);}
389
389
  const CHAT_SHORTCUT_TARGET='agents-relay-shortcut';
390
390
  function chatShortcutField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('ChatGPT shortcut')));const select=document.createElement('select');select.className='job-filter job-chat-shortcut-select';select.setAttribute('aria-label','ChatGPT shortcut');select.append(new Option('',''));for(const item of parseChatShortcuts(chatShortcutText()))select.append(new Option(item.name,item.prompt));select.value='';select.addEventListener('change',()=>{if(!select.value)return;const prompt=select.value.replaceAll('%1',jobChatRef(job));window.open(chatShortcutUrl(prompt),CHAT_SHORTCUT_TARGET,'noopener');select.value='';});box.append(select);return box;}
@@ -0,0 +1,18 @@
1
+ /** Report terminal projection faults once per Job version and reason while still retrying delivery. */
2
+ export class ProjectionFailureDeduplicator {
3
+ seen = new Map();
4
+ shouldReport(jobId, updatedAt, reason, classification) {
5
+ if (classification !== 'terminal') {
6
+ this.seen.delete(jobId);
7
+ return true;
8
+ }
9
+ const signature = `${updatedAt}\0${reason}`;
10
+ if (this.seen.get(jobId) === signature)
11
+ return false;
12
+ this.seen.delete(jobId);
13
+ this.seen.set(jobId, signature);
14
+ if (this.seen.size > 4096)
15
+ this.seen.delete(this.seen.keys().next().value);
16
+ return true;
17
+ }
18
+ }
package/dist/relayd.js CHANGED
@@ -18,6 +18,7 @@ import { createJobIdRegistryResolver } from './local-job-registry.js';
18
18
  import { projectAuthoritativeLocalMerge } from './local-merge-projection.js';
19
19
  import { archiveDisplayPullRequest } from './github-pr-archive.js';
20
20
  import { orderGitHubBindingCandidates } from './github-binding-order.js';
21
+ import { ProjectionFailureDeduplicator } from './projection-failure-dedupe.js';
21
22
  import { authoritativeBranchPushArgs } from './github-branch-projection.js';
22
23
  import { githubAuthContext } from './github-auth.js';
23
24
  import { DisabledGitHubClient, noGitHubMode } from './no-github.js';
@@ -207,6 +208,7 @@ export async function runDaemon(argv) {
207
208
  githubAuthError = errorMessage(error);
208
209
  }
209
210
  };
211
+ const projectionFailureDeduplicator = new ProjectionFailureDeduplicator();
210
212
  const syncGitHubBindings = async () => {
211
213
  if (noGitHub)
212
214
  return;
@@ -221,6 +223,8 @@ export async function runDaemon(argv) {
221
223
  const snapshot = { path: candidate.snapshotPath };
222
224
  const discovered = candidate.job;
223
225
  try {
226
+ if (discovered.state === 'CANCELLED')
227
+ continue;
224
228
  if (!repositoryPaths.has(discovered.repository) || !discovered.localMerge?.sourceRef || !discovered.localMerge?.targetRef)
225
229
  continue;
226
230
  const sourceRef = discovered.localMerge.sourceRef;
@@ -249,7 +253,9 @@ export async function runDaemon(argv) {
249
253
  const terminal = ['COMPLETED', 'CANCELLED'].includes(discovered.state);
250
254
  if (terminal && existingExternalPr <= 0)
251
255
  continue;
252
- const sourceSha = await gitText(canonical, ['rev-parse', '--verify', sourceRef]);
256
+ const sourceSha = await gitText(canonical, ['rev-parse', '--verify', sourceRef]).catch(() => '');
257
+ if (!sourceSha)
258
+ continue;
253
259
  const facts = await relay.loadPullRequestFacts();
254
260
  if (facts?.head?.sha !== sourceSha) {
255
261
  await gitText(canonical, ['fetch', '--no-tags', 'origin',
@@ -321,12 +327,15 @@ export async function runDaemon(argv) {
321
327
  }
322
328
  }
323
329
  catch (error) {
324
- await bus?.publish(eventFor(discovered.id, 'projection', null, 'projection.delivery_failed', 'failed', `GitHub projection failed: ${errorMessage(error)}`, 'orchestrator', { operation: 'github.projectLocalMerge', error: errorMessage(error) }, { agent: 'relay', component: 'github-projection' }))?.catch(() => { });
330
+ const reason = errorMessage(error), classification = classifyFailure(error).classification;
331
+ if (!projectionFailureDeduplicator.shouldReport(discovered.id, discovered.updatedAt, reason, classification))
332
+ continue;
333
+ await bus?.publish(eventFor(discovered.id, 'projection', null, 'projection.delivery_failed', 'failed', `GitHub projection failed: ${errorMessage(error)}`, 'orchestrator', { operation: 'github.projectLocalMerge', error: reason }, { agent: 'relay', component: 'github-projection' }))?.catch(() => { });
325
334
  observability().record({
326
335
  stage: 'projection', event: 'github.binding_job_failed', level: 'warn', outcome: 'failed',
327
- message: `GitHub binding/projection failed for ${discovered.id}: ${errorMessage(error)}`,
336
+ message: `GitHub binding/projection failed for ${discovered.id}: ${reason}`,
328
337
  correlation: { repository: discovered.repository, prNumber: discovered.externalPrNumber ?? discovered.prNumber, jobId: discovered.id },
329
- data: { classification: classifyFailure(error).classification, reason: errorMessage(error) },
338
+ data: { classification, reason },
330
339
  });
331
340
  }
332
341
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.78",
3
+ "version": "2.0.79",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {