agents-relay 2.0.77 → 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.
- package/dist/dashboard-html.js +2 -2
- package/dist/pool.js +13 -3
- package/dist/projection-failure-dedupe.js +18 -0
- package/dist/relayd.js +13 -4
- package/package.json +1 -1
package/dist/dashboard-html.js
CHANGED
|
@@ -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(
|
|
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)+
|
|
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;}
|
package/dist/pool.js
CHANGED
|
@@ -152,9 +152,19 @@ export class RepositoryWorkerPool {
|
|
|
152
152
|
if (!needsLifecycleReconciliation(job))
|
|
153
153
|
continue;
|
|
154
154
|
const allowance = Math.max(1, Math.min(capacity, ready || (expiredRunning ? 1 : 0) || 1));
|
|
155
|
-
|
|
156
|
-
await this.
|
|
157
|
-
|
|
155
|
+
try {
|
|
156
|
+
await this.reconcileExclusive(job.id, async () => {
|
|
157
|
+
await this.makeReconciler(store, allowance).reconcile(job.id);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
observability().record({
|
|
162
|
+
stage: 'reconcile', event: 'reconcile.job_failed', level: 'error', outcome: 'failed',
|
|
163
|
+
message: `Reconcile failed for ${job.id}: ${errorMessage(error)}`,
|
|
164
|
+
correlation: { jobId: job.id, repository: job.repository, prNumber: job.prNumber },
|
|
165
|
+
data: { classification: 'job-isolated' },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
158
168
|
capacity -= allowance;
|
|
159
169
|
}
|
|
160
170
|
}
|
|
@@ -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
|
-
|
|
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}: ${
|
|
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
|
|
338
|
+
data: { classification, reason },
|
|
330
339
|
});
|
|
331
340
|
}
|
|
332
341
|
}
|