agents-relay 1.0.70 → 1.0.72
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.js +31 -8
- package/dist/pool.js +3 -3
- package/package.json +1 -1
package/dist/dashboard.js
CHANGED
|
@@ -80,6 +80,8 @@ export const dashboardHtml = `<!doctype html>
|
|
|
80
80
|
.panel-actions { display:flex; align-items:center; gap:.45rem; }
|
|
81
81
|
.job-filters { display:flex; gap:.35rem; align-items:center; flex-wrap:wrap; }
|
|
82
82
|
.job-filter { max-width:145px; border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--text); padding:.27rem .4rem; font:inherit; font-size:.72rem; }
|
|
83
|
+
.quick-filter { border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--muted); padding:.27rem .42rem; font:inherit; font-size:.7rem; font-weight:750; cursor:pointer; }
|
|
84
|
+
.quick-filter[aria-pressed="true"] { border-color:var(--accent); background:var(--accent2); color:var(--accent); }
|
|
83
85
|
.refresh-button { border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--muted); padding:.28rem .55rem; font:inherit; font-size:.72rem; font-weight:700; cursor:pointer; }
|
|
84
86
|
.refresh-button:disabled { cursor:default; opacity:.55; }
|
|
85
87
|
.panel-body { padding:.75rem; overflow:auto; max-height:calc(100vh - 11.5rem); }
|
|
@@ -88,14 +90,13 @@ export const dashboardHtml = `<!doctype html>
|
|
|
88
90
|
.workspace { display:grid; gap:1rem; }
|
|
89
91
|
.job-list,.tree,.event-list { list-style:none; margin:0; padding:0; }
|
|
90
92
|
.job-list { display:grid; gap:.35rem; }
|
|
91
|
-
.job-row { display:grid; gap:.2rem; padding:.35rem .45rem; border:1px solid transparent; border-radius:.4rem; }
|
|
93
|
+
.job-row { display:grid; gap:.2rem; padding:.35rem .45rem; border:1px solid transparent; border-radius:.4rem; cursor:pointer; }
|
|
92
94
|
.job-row[aria-current="true"] { border-color:var(--accent); background:var(--accent2); }
|
|
93
95
|
.job-row.complete-unmerged { border-color:#d69a1b; background:#fff8df; }
|
|
94
96
|
.job-row.complete-unmerged[aria-current="true"] { border-color:#b77900; background:#fff0bd; }
|
|
95
|
-
.job-
|
|
97
|
+
.job-content { min-width:0; display:grid; gap:.2rem; }
|
|
98
|
+
.job-button { min-width:0; display:block; padding:.12rem 0; border:0; background:transparent; color:inherit; text-align:left; font:inherit; cursor:pointer; }
|
|
96
99
|
.job-title { font-weight:750; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
97
|
-
.job-meta { display:flex; gap:.4rem; flex-wrap:wrap; color:var(--muted); font-size:.7rem; }
|
|
98
|
-
.job-meta span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
99
100
|
.job-last-line { display:flex; align-items:center; justify-content:space-between; gap:.5rem; min-width:0; }
|
|
100
101
|
.job-last-line .subtle:first-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
101
102
|
.job-last-line .subtle:last-child { flex:none; white-space:nowrap; }
|
|
@@ -157,7 +158,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
157
158
|
</header>
|
|
158
159
|
<main>
|
|
159
160
|
<section id="jobs-view" role="tabpanel" aria-labelledby="jobs-tab" class="panel jobs-panel">
|
|
160
|
-
<div class="panel-head"><h2>Jobs</h2><div class="panel-actions"><div class="job-filters"><select id="repo-filter" class="job-filter" aria-label="Filter jobs by repository"><option value="">All repos</option></select></div><button id="jobs-refresh" class="refresh-button" type="button">Refresh</button></div></div>
|
|
161
|
+
<div class="panel-head"><h2>Jobs</h2><div class="panel-actions"><div class="job-filters"><button id="quick-jobs" class="quick-filter" type="button" aria-pressed="true">JOBS</button><select id="quick-status" class="job-filter" aria-label="Filter jobs by derived status"><option value="">STATUS</option></select><button id="quick-autonomous" class="quick-filter" type="button" aria-pressed="false">AUTONOMOUS</button><button id="quick-draft" class="quick-filter" type="button" aria-pressed="false">DRAFT</button><select id="repo-filter" class="job-filter" aria-label="Filter jobs by repository"><option value="">All repos</option></select></div><button id="jobs-refresh" class="refresh-button" type="button">Refresh</button></div></div>
|
|
161
162
|
<div class="panel-body" aria-live="polite"><ul id="job-list" class="job-list"><li class="skeleton" aria-hidden="true"></li></ul></div>
|
|
162
163
|
</section>
|
|
163
164
|
<div class="workspace" id="jobs-workspace">
|
|
@@ -206,6 +207,10 @@ const jobsWorkspace=document.querySelector('#jobs-workspace');
|
|
|
206
207
|
const jobList=document.querySelector('#job-list');
|
|
207
208
|
const jobsRefresh=document.querySelector('#jobs-refresh');
|
|
208
209
|
const repoFilter=document.querySelector('#repo-filter');
|
|
210
|
+
const quickJobs=document.querySelector('#quick-jobs');
|
|
211
|
+
const quickStatus=document.querySelector('#quick-status');
|
|
212
|
+
const quickAutonomous=document.querySelector('#quick-autonomous');
|
|
213
|
+
const quickDraft=document.querySelector('#quick-draft');
|
|
209
214
|
const selectedJobRefresh=document.querySelector('#selected-job-refresh');
|
|
210
215
|
const jobDetail=document.querySelector('#job-detail');
|
|
211
216
|
const taskTree=document.querySelector('#task-tree');
|
|
@@ -222,6 +227,8 @@ let jobs=[];
|
|
|
222
227
|
let selectedJob=sessionStorage.getItem('agents-relay:selected-job');
|
|
223
228
|
let selectedTaskId=sessionStorage.getItem('agents-relay:selected-task');
|
|
224
229
|
let selectedRepo=sessionStorage.getItem('agents-relay:repo-filter')||'';
|
|
230
|
+
let quickMode=sessionStorage.getItem('agents-relay:quick-filter')||'jobs';
|
|
231
|
+
let selectedRuntimeStatus=sessionStorage.getItem('agents-relay:runtime-status-filter')||'';
|
|
225
232
|
let eventTaskId=selectedTaskId;
|
|
226
233
|
const displayedEventIds=new Set();
|
|
227
234
|
|
|
@@ -241,20 +248,31 @@ function chatField(task){const box=element('div','detail-group');box.append(elem
|
|
|
241
248
|
function clearEvents(){displayedEventIds.clear();eventFeed.replaceChildren(element('li','empty',text('Waiting for events.')));}
|
|
242
249
|
async function loadEventHistory(){if(!selectedJob){clearEvents();return;}const params=new URLSearchParams({job:selectedJob,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();}}
|
|
243
250
|
function selectJob(id){selectedJob=id;selectedTaskId=null;eventTaskId=null;rememberSelection();clearEvents();location.hash=id?'#/jobs/'+encodeURIComponent(id):'#/jobs';renderAll();void loadEventHistory();}
|
|
244
|
-
function
|
|
251
|
+
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;}
|
|
252
|
+
function filteredJobs(){return jobs.filter(job=>(!selectedRepo||job.repository===selectedRepo)&&matchesQuickFilter(job));}
|
|
253
|
+
function setQuickMode(mode,status=''){quickMode=mode;selectedRuntimeStatus=mode==='status'?status:'';sessionStorage.setItem('agents-relay:quick-filter',quickMode);if(selectedRuntimeStatus)sessionStorage.setItem('agents-relay:runtime-status-filter',selectedRuntimeStatus);else sessionStorage.removeItem('agents-relay:runtime-status-filter');syncFilters();renderJobList();}
|
|
245
254
|
function syncFilters(){
|
|
246
255
|
const repos=[...new Set(jobs.map(job=>job.repository).filter(Boolean))].sort();
|
|
247
256
|
const previousRepo=selectedRepo;
|
|
248
257
|
repoFilter.replaceChildren(new Option('All repos',''),...repos.map(repo=>new Option(repo,repo)));
|
|
249
258
|
selectedRepo=repos.includes(previousRepo)?previousRepo:'';
|
|
250
259
|
repoFilter.value=selectedRepo;
|
|
260
|
+
const statuses=[...new Set(jobs.map(job=>job.runtimeStatus||'IDLE'))].sort();
|
|
261
|
+
const previousStatus=selectedRuntimeStatus;
|
|
262
|
+
quickStatus.replaceChildren(new Option('STATUS',''),...statuses.map(status=>new Option(status,status)));
|
|
263
|
+
selectedRuntimeStatus=statuses.includes(previousStatus)?previousStatus:'';
|
|
264
|
+
if(quickMode==='status'&&!selectedRuntimeStatus)quickMode='jobs';
|
|
265
|
+
quickStatus.value=quickMode==='status'?selectedRuntimeStatus:'';
|
|
266
|
+
quickJobs.setAttribute('aria-pressed',String(quickMode==='jobs'));
|
|
267
|
+
quickAutonomous.setAttribute('aria-pressed',String(quickMode==='autonomous'));
|
|
268
|
+
quickDraft.setAttribute('aria-pressed',String(quickMode==='draft'));
|
|
251
269
|
}
|
|
252
270
|
function actionEnabled(job,action){if(action==='start')return job.state==='DRAFT'&&job.githubState!=='CLOSED'&&job.githubState!=='MERGED';if(action==='reconcile')return job.state==='ACTIVE'&&job.githubState==='OPEN';if(action==='cancel')return job.state!=='COMPLETED'&&job.state!=='CANCELLED'&&job.githubState==='OPEN';return false;}
|
|
253
271
|
function actionButton(job,action,label,icon){const button=document.createElement('button');button.type='button';button.className='job-action';button.textContent=icon;button.title=label;button.setAttribute('aria-label',label);button.disabled=!actionEnabled(job,action);button.addEventListener('click',event=>{event.stopPropagation();void runJobAction(job,action,button);});return button;}
|
|
254
272
|
async function runJobAction(job,action,button){button.disabled=true;try{const response=await fetch('/api/job-action?id='+encodeURIComponent(job.id)+'&action='+encodeURIComponent(action),{method:'POST'});if(!response.ok)throw new Error(response.status+' '+response.statusText);await refresh(true);}catch(error){button.title=labelForAction(action)+': '+error.message;}finally{renderAll();}}
|
|
255
273
|
function labelForAction(action){return action==='start'?'Start':action==='reconcile'?'Reconcile':'Cancel';}
|
|
256
274
|
function repoName(repository){if(typeof repository!=='string'||!repository)return 'unknown-repo';const parts=repository.split('/').filter(Boolean);return parts.at(-1)||repository;}
|
|
257
|
-
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected repo.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(job.id===selectedJob));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(job.id===selectedJob));select.title=job.title||job.id;select.addEventListener('click',()
|
|
275
|
+
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected repo.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(job.id===selectedJob));row.addEventListener('click',()=>selectJob(job.id));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(job.id===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job.id);});select.append(element('span','job-title',text(repoName(job.repository)+'/PR#'+value(job.prNumber)+': '+value(job.title||job.id))));const states=element('span','job-status-line');states.append(element('span','badge '+value(job.state),text(value(job.state))),element('span','badge '+value(job.runtimeStatus||'IDLE'),text(value(job.runtimeStatus||'IDLE'))),element('span','badge '+value(job.githubState||'UNKNOWN'),text(value(job.githubState||'UNKNOWN'))));if(job.executionMode==='autonomous')states.append(element('span','badge flag-autonomous',text('AUTONOMOUS')));if(job.reviewEnabled!==false)states.append(element('span','badge flag-review',text('REVIEW REQUIRED')));if(isCompleteUnmerged(job))states.append(element('span','badge MERGE_PENDING',text('MERGE PENDING')));const actions=element('span','job-actions');actions.append(actionButton(job,'start','Start','▶'),actionButton(job,'reconcile','Reconcile','↻'),actionButton(job,'cancel','Cancel','×'));const primary=element('div','job-primary');primary.append(states,actions);const lastLine=element('div','job-last-line');lastLine.append(element('span','subtle',text(compactCounts(counts(job.tasks)))),element('span','subtle',text(relativeTime(job.updatedAt))));content.append(select,primary,lastLine);row.append(content);jobList.append(element('li','',row));}}
|
|
258
276
|
function taskNode(task,children,depth){const li=element('li');const row=document.createElement('button');row.type='button';row.className='task-row';row.setAttribute('aria-current',String(task.id===selectedTaskId));row.title=task.id;row.addEventListener('click',()=>{selectedTaskId=task.id;eventTaskId=task.id;rememberSelection();clearEvents();renderAll();void loadEventHistory();});const id=element('span','task-id',text(task.id));id.style.paddingLeft=Math.min(depth,4)*6+'px';row.append(id,element('span','badge '+value(task.state),text(task.state)));li.append(row);const nested=children.get(task.id)||[];if(nested.length){const ul=element('ul','tree');for(const child of nested)ul.append(taskNode(child,children,depth+1));li.append(ul);}return li;}
|
|
259
277
|
function buildTree(tasks){taskTree.replaceChildren();if(!Array.isArray(tasks)||!tasks.length){taskTree.append(element('li','empty',text('No tasks in durable state.')));return;}const known=new Set(tasks.map(task=>task.id));const children=new Map();const roots=[];for(const task of tasks){if(!task.parentTaskId||!known.has(task.parentTaskId))roots.push(task);else{const siblings=children.get(task.parentTaskId)||[];siblings.push(task);children.set(task.parentTaskId,siblings);}}for(const task of roots)taskTree.append(taskNode(task,children,0));}
|
|
260
278
|
function attemptBudget(task){const attempt=Number(task?.attempt)||0;const max=Number(task?.maxAttempts)||0;if(max>=3&&max%3===0){const effectiveMax=Math.max(max,Math.ceil(attempt/3)*3);return attempt+' / 3×'+(effectiveMax/3);}return attempt+' / '+max;}
|
|
@@ -264,7 +282,8 @@ function renderSelectedJob(){jobDetail.replaceChildren();const job=selected();if
|
|
|
264
282
|
function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();}
|
|
265
283
|
function isCompleteUnmerged(job){return job.runtimeStatus==='MERGE_PENDING';}
|
|
266
284
|
function isTerminalJob(job){return ['COMPLETED','CANCELLED'].includes(job.state)||['MERGED','CLOSED'].includes(job.githubState);}
|
|
267
|
-
function
|
|
285
|
+
function jobGroup(job){if(isTerminalJob(job))return 3;if(job.executionMode==='autonomous')return 1;if(job.draft||job.state==='DRAFT')return 2;return 0;}
|
|
286
|
+
function compareJobs(a,b){const groupDelta=jobGroup(a)-jobGroup(b);if(groupDelta)return groupDelta;const mergePendingDelta=Number(isCompleteUnmerged(b))-Number(isCompleteUnmerged(a));return mergePendingDelta||String(b.updatedAt).localeCompare(String(a.updatedAt));}
|
|
268
287
|
async function loadJSON(url){const response=await fetch(url);if(!response.ok)throw new Error(response.status+' '+response.statusText);return response.json();}
|
|
269
288
|
function renderRateLimitStatus(status){
|
|
270
289
|
if(!status?.limited){rateLimitWarning.hidden=true;rateLimitWarning.textContent='';return;}
|
|
@@ -288,6 +307,10 @@ jobsTab.addEventListener('click',()=>{location.hash=selectedJob?'#/jobs/'+encode
|
|
|
288
307
|
jobsRefresh.addEventListener('click',()=>void refresh(true));
|
|
289
308
|
eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
|
|
290
309
|
repoFilter.addEventListener('change',()=>{selectedRepo=repoFilter.value;sessionStorage.setItem('agents-relay:repo-filter',selectedRepo);renderJobList();});
|
|
310
|
+
quickJobs.addEventListener('click',()=>setQuickMode('jobs'));
|
|
311
|
+
quickAutonomous.addEventListener('click',()=>setQuickMode('autonomous'));
|
|
312
|
+
quickDraft.addEventListener('click',()=>setQuickMode('draft'));
|
|
313
|
+
quickStatus.addEventListener('change',()=>setQuickMode(quickStatus.value?'status':'jobs',quickStatus.value));
|
|
291
314
|
selectedJobRefresh.addEventListener('click',()=>void refreshSelected());
|
|
292
315
|
usageTab.addEventListener('click',()=>{location.hash='#/usage';showView('usage');});
|
|
293
316
|
window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){event.preventDefault();showView(location.hash==='#/usage'?'jobs':'usage');}});
|
package/dist/pool.js
CHANGED
|
@@ -81,7 +81,7 @@ export class RepositoryWorkerPool {
|
|
|
81
81
|
const store = new GitHubStore(this.client, repository, prNumber, this.trustedAuthors);
|
|
82
82
|
await this.makeReconciler(store, Math.max(1, this.concurrency)).reconcile(jobId);
|
|
83
83
|
}
|
|
84
|
-
async
|
|
84
|
+
async handleLifecycleEvent(event) {
|
|
85
85
|
const target = this.jobTargets.get(event.job_id);
|
|
86
86
|
if (!target || !this.repositories.includes(target.repository)) {
|
|
87
87
|
process.stderr.write(`warning: terminal event target unknown for job ${event.job_id}\n`);
|
|
@@ -92,8 +92,8 @@ export class RepositoryWorkerPool {
|
|
|
92
92
|
}
|
|
93
93
|
async watch(bus) {
|
|
94
94
|
return bus.subscribeAll(async (event) => {
|
|
95
|
-
if (['task.completed', 'task.failed', 'task.blocked', 'review.started', 'review.approved', 'review.changes_required', 'review.blocked'].includes(event.type)) {
|
|
96
|
-
await this.
|
|
95
|
+
if (['task.started', 'task.completed', 'task.failed', 'task.blocked', 'task.cancelled', 'review.started', 'review.approved', 'review.changes_required', 'review.blocked'].includes(event.type)) {
|
|
96
|
+
await this.handleLifecycleEvent(event);
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
if (event.type === 'job.wake' || event.type === 'github.webhook') {
|