agents-relay 1.0.159 → 1.0.160

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/cli.js CHANGED
@@ -7,8 +7,8 @@ import { dashboardManagedJobs, InMemoryStore, GitHubStore, resolvePullRequest, l
7
7
  import { githubAuthContext } from './github-auth.js';
8
8
  import { taskOverallTimeoutMs } from './types.js';
9
9
  import { Reconciler } from './reconciler.js';
10
- import { dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
11
- export { dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
10
+ import { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
11
+ export { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
12
12
  import { CodexAdapter, ChatGptAdapter } from './adapters.js';
13
13
  import { CodexThreadContinuation, CommandContinuation, WebhookContinuation } from './continuation.js';
14
14
  import { eventFor } from './events.js';
@@ -488,6 +488,7 @@ export async function createService(args) {
488
488
  start: dashboardJobAction(loaded.store, reconciler, 'start'),
489
489
  reconcile: dashboardJobAction(loaded.store, reconciler, 'reconcile'),
490
490
  cancel: dashboardJobAction(loaded.store, reconciler, 'cancel'),
491
+ jobSetState: dashboardJobSetState(loaded.store),
491
492
  taskRetry: dashboardTaskAction(loaded.store, reconciler, 'retry'),
492
493
  taskCancel: dashboardTaskAction(loaded.store, reconciler, 'cancel'),
493
494
  taskSetState: dashboardTaskSetState(loaded.store),
@@ -1,5 +1,5 @@
1
1
  import { extendRetryBudget, transitionTaskEvent } from './scheduler.js';
2
- import { applyTaskWorkflowDecision } from './workflow/runtime.js';
2
+ import { applyJobWorkflowDecision, applyTaskWorkflowDecision } from './workflow/runtime.js';
3
3
  export function dashboardTaskAction(store, reconciler, action) {
4
4
  return async (jobId, taskId) => {
5
5
  const job = await store.load(jobId);
@@ -26,6 +26,24 @@ export function dashboardTaskAction(store, reconciler, action) {
26
26
  return await reconciler.wake(job.id);
27
27
  };
28
28
  }
29
+ export function dashboardJobSetState(store) {
30
+ return async (jobId, state) => {
31
+ const job = await store.load(jobId);
32
+ if (job.state === state)
33
+ return job;
34
+ applyJobWorkflowDecision(job, {
35
+ accepted: true,
36
+ target: state,
37
+ guard: null,
38
+ actions: [],
39
+ mutations: [],
40
+ effects: [],
41
+ reason: 'accepted',
42
+ });
43
+ await store.saveJob(job);
44
+ return await store.load(job.id);
45
+ };
46
+ }
29
47
  export function dashboardTaskSetState(store) {
30
48
  return async (jobId, taskId, state) => {
31
49
  const job = await store.load(jobId);
@@ -324,13 +324,16 @@ function taskTagDisplay(job,task){const display=task.tag&&job?.tag?.values?.[tas
324
324
  function taskNode(task,children,depth){const li=element('li');const nested=children.get(task.id)||[];const line=element('div','task-line');if(nested.length){const key=(selectedJob||'')+':'+task.id;const collapsed=collapsedTasks.has(key);const fold=document.createElement('button');fold.type='button';fold.className='task-fold';fold.setAttribute('aria-expanded',String(!collapsed));fold.setAttribute('aria-label',(collapsed?'Expand ':'Collapse ')+task.id);fold.textContent=collapsed?'▸':'▾';line.append(fold);const ul=element('ul','tree');ul.hidden=collapsed;for(const child of nested)ul.append(taskNode(child,children,depth+1));fold.addEventListener('click',()=>{const next=!ul.hidden;ul.hidden=next;fold.setAttribute('aria-expanded',String(!next));fold.setAttribute('aria-label',(next?'Expand ':'Collapse ')+task.id);fold.textContent=next?'▸':'▾';if(next)collapsedTasks.add(key);else collapsedTasks.delete(key);});li.append(line,ul);}else li.append(line);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',()=>{const deselect=selectedTaskId===task.id;selectedTaskId=deselect?null:task.id;eventTaskId=deselect?null: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)));const tag=taskTagDisplay(selected(),task);if(tag){const badge=element('span','task-tag',text(tag));badge.title=(selected()?.tag?.description||'Task tag')+': '+task.tag;row.append(badge);}line.append(row);return li;}
325
325
  function buildTree(tasks){taskTree.replaceChildren();if(!Array.isArray(tasks)||!tasks.length){taskTree.append(element('li','empty',text('No tasks in durable state.')));return;}const work=tasks.filter(task=>task.kind!=='planner');if(!work.length){taskTree.append(element('li','empty',text('No work tasks in durable state.')));return;}const known=new Set(work.map(task=>task.id));const children=new Map();const roots=[];for(const task of work){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));}
326
326
  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;}
327
+ const JOB_STATES=['DRAFT','ACTIVE','COMPLETED','CANCELLED'];
328
+ async function setJobState(job,state,select){if(!job||state===job.state)return;select.disabled=true;try{const response=await fetch('/api/job-state?id='+encodeURIComponent(job.id)+'&state='+encodeURIComponent(state),{method:'POST'});if(!response.ok)throw new Error(response.status+' '+response.statusText);const updated=await response.json();const index=jobs.findIndex(candidate=>candidate.id===job.id);if(index>=0)jobs[index]=updated;else jobs.push(updated);renderAll();void loadEventHistory();}catch(error){select.title='Set job lifecycle state: '+error.message;select.value=job.state;}finally{select.disabled=false;}}
329
+ function jobStateField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('Lifecycle state')));const select=document.createElement('select');select.className='job-filter job-state-select';select.setAttribute('aria-label','Job lifecycle state');for(const state of JOB_STATES)select.append(new Option(state,state));select.value=job.state;select.addEventListener('change',()=>void setJobState(job,select.value,select));box.append(select);return box;}
327
330
  const TASK_STATES=['DRAFT','QUEUED','RUNNING','BLOCKED','SUCCEEDED','FAILED','CANCELLED'];
328
331
  async function setTaskState(task,state,select){const job=selected();if(!job||state===task.state)return;select.disabled=true;try{const response=await fetch('/api/task-state?id='+encodeURIComponent(job.id)+'&task='+encodeURIComponent(task.id)+'&state='+encodeURIComponent(state),{method:'POST'});if(!response.ok)throw new Error(response.status+' '+response.statusText);const updated=await response.json();const index=jobs.findIndex(candidate=>candidate.id===job.id);if(index>=0)jobs[index]=updated;else jobs.push(updated);renderAll();void loadEventHistory();}catch(error){select.title='Set task state: '+error.message;select.value=task.state;}finally{select.disabled=false;}}
329
332
  function taskStateField(task){const box=element('div','detail-group');box.append(element('p','detail-label',text('State')));const select=document.createElement('select');select.className='job-filter task-state-select';select.setAttribute('aria-label','Task state');for(const state of TASK_STATES)select.append(new Option(state,state));select.value=task.state;select.addEventListener('change',()=>void setTaskState(task,select.value,select));box.append(select);return box;}
330
333
  function manualOverrideField(task){const rows=Array.isArray(task.manualStateOverrides)?task.manualStateOverrides:[];const last=rows.at(-1);return field('Manual override',last?last.from+' → '+last.to+' · '+relativeTime(last.at):'—');}
331
334
  function renderTaskDetail(task){taskDetailRoot.replaceChildren();if(!task){taskDetailRoot.append(element('p','empty',text('Select a task to inspect execution details.')));return;}const routing=task.routing||{};const groups=element('div','detail-groups');groups.append(field('Task',task.id),field('Kind',task.kind||'work'),taskStateField(task),manualOverrideField(task),field('Planning decision',task.planningDecisionId||'—'),field('Planning source',task.planningSource||'—'),field('Planning round',task.planningRound===undefined?'—':String(task.planningRound)),field('Project / agent',value(task.projectName)+' / '+value(task.agentName)),field('Parent task',task.parentTaskId||'root'),field('Adapter',task.adapter),field('Provider / model',value(routing.provider)+' / '+value(routing.model)),field('Reasoning / profile',value(routing.reasoning||routing.profile)),field('Attempt',attemptBudget(task)),field('Dependencies',Array.isArray(task.dependencies)&&task.dependencies.length?task.dependencies.join(', '):'none'),taskCommentField(task),chatField(task));const job=selected();if(task.reviewHeadSha&&job)groups.append(commitField('HEAD commit',job,task.reviewHeadSha));const graph=agentGraphField(task);if(graph)groups.append(graph);if(task.plannerResult)groups.append(field('Objective status',task.plannerResult.objective_status),markdownField('Assessment',task.plannerResult.assessment));if(task.error)groups.append(field('Error',task.error));taskDetailRoot.append(groups);}
332
335
  function reviewStatus(job){const review=job?.review;if(!review)return 'Not started';if(review.outcome==='approved'&&review.headSha===job.headSha)return 'MERGE READY · approved';const stale=Array.isArray(job?.tasks)&&job.tasks.some(task=>task.agentName==='reviewer'&&task.reviewHeadSha&&job.headSha&&task.reviewHeadSha!==job.headSha);if(stale)return 'STALE · awaiting review';return String(review.outcome||'pending').replace('_',' ').toUpperCase();}
333
- function renderSelectedJob(){jobDetail.replaceChildren();const job=selected();if(!job){selectedJobTitle.textContent='';jobDetail.append(element('div','empty',text('No durable jobs discovered.')));buildTree([]);renderTaskDetail(null);return;}selectedJobTitle.textContent=value(job.title||job.id);if(job.id!==selectedJob){selectedJob=job.id;selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}const summary=element('div','job-summary');summary.append(field('PR state',job.githubState||'UNKNOWN'),field('Lifecycle state',job.state),field('Runtime status',job.runtimeStatus||'IDLE'),field('Execution mode',job.executionMode||'fixed'),field('Cadence',cadence(job)),field('Review',reviewStatus(job)),commitField('Review head SHA',job,job.review?.headSha||job.headSha||''),prField(job),field('Repository',job.repository||'—'),field('Last activity',relativeTime(job.updatedAt)),field('Task states',compactCounts(job.tasks)));jobDetail.append(summary);const selectedTask=(job.tasks||[]).find(task=>task.id===selectedTaskId)||null;if(selectedTaskId&&!selectedTask){selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}buildTree(job.tasks||[]);renderTaskDetail(selectedTask);renderEventScope();}
336
+ function renderSelectedJob(){jobDetail.replaceChildren();const job=selected();if(!job){selectedJobTitle.textContent='';jobDetail.append(element('div','empty',text('No durable jobs discovered.')));buildTree([]);renderTaskDetail(null);return;}selectedJobTitle.textContent=value(job.title||job.id);if(job.id!==selectedJob){selectedJob=job.id;selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}const summary=element('div','job-summary');summary.append(field('PR state',job.githubState||'UNKNOWN'),jobStateField(job),field('Runtime status',job.runtimeStatus||'IDLE'),field('Execution mode',job.executionMode||'fixed'),field('Cadence',cadence(job)),field('Review',reviewStatus(job)),commitField('Review head SHA',job,job.review?.headSha||job.headSha||''),prField(job),field('Repository',job.repository||'—'),field('Last activity',relativeTime(job.updatedAt)),field('Task states',compactCounts(job.tasks)));jobDetail.append(summary);const selectedTask=(job.tasks||[]).find(task=>task.id===selectedTaskId)||null;if(selectedTaskId&&!selectedTask){selectedTaskId=null;eventTaskId=null;clearEvents();rememberSelection();}buildTree(job.tasks||[]);renderTaskDetail(selectedTask);renderEventScope();}
334
337
  function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();syncMobileJobsView();}
335
338
  function isCompleteUnmerged(job){return job.runtimeStatus==='MERGE_PENDING';}
336
339
  function isTerminalJob(job){return ['COMPLETED','CANCELLED'].includes(job.state)||['MERGED','CLOSED'].includes(job.githubState);}
package/dist/dashboard.js CHANGED
@@ -269,6 +269,31 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
269
269
  }
270
270
  return;
271
271
  }
272
+ if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/job-state') {
273
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
274
+ const requestedJobId = url.searchParams.get('id') ?? '';
275
+ const requestedState = url.searchParams.get('state') ?? '';
276
+ const validState = ['DRAFT', 'ACTIVE', 'COMPLETED', 'CANCELLED'].includes(requestedState);
277
+ if (req.method !== 'POST' || !/^[A-Za-z0-9_-]+$/.test(requestedJobId) || !validState || !resolvedActions?.jobSetState) {
278
+ res.writeHead(404);
279
+ res.end('Job state override unavailable');
280
+ return;
281
+ }
282
+ try {
283
+ const result = await resolvedActions.jobSetState(requestedJobId, requestedState);
284
+ if (!res.headersSent)
285
+ res.writeHead(200, { 'content-type': 'application/json' });
286
+ if (!res.writableEnded)
287
+ res.end(JSON.stringify(result));
288
+ }
289
+ catch (error) {
290
+ if (!res.headersSent)
291
+ res.writeHead(409);
292
+ if (!res.writableEnded)
293
+ res.end(error instanceof Error ? error.message : String(error));
294
+ }
295
+ return;
296
+ }
272
297
  if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/task-state') {
273
298
  const url = new URL(req.url ?? '/', 'http://127.0.0.1');
274
299
  const requestedJobId = url.searchParams.get('id') ?? '';
package/dist/relayd.js CHANGED
@@ -12,7 +12,7 @@ import { Reconciler } from './reconciler.js';
12
12
  import { dashboardManagedJobs, GitHubStore, InMemoryStore, loadManagedGitHubPullRequestSnapshotGraphql, withGitHubRequestContext } from './store.js';
13
13
  import { githubAuthContext } from './github-auth.js';
14
14
  import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
15
- import { dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
15
+ import { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
16
16
  import { codexAndZaiUsageRegistry } from './usage.js';
17
17
  import { defaultWorkspaceRoot, discoverWorkspaceRepositories, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
18
18
  import { deriveJobRuntimeStatus } from './runtime-status.js';
@@ -200,6 +200,15 @@ export async function runDaemon(argv) {
200
200
  }
201
201
  return await dashboardJobAction(store, makeReconciler(store, concurrency), action)(jobId);
202
202
  };
203
+ const workspaceJobSetState = async (jobId, state) => {
204
+ const overview = cachedOverviews.find(item => item.job.id === jobId);
205
+ if (!overview)
206
+ throw new Error(`Managed job ${jobId} not found`);
207
+ const store = new GitHubStore(client, overview.job.repository, overview.job.prNumber, trusted);
208
+ const result = await dashboardJobSetState(store)(jobId, state);
209
+ await refreshPullRequest(overview.job.repository, overview.job.prNumber, 'dashboard.job-state', { force: true });
210
+ return result;
211
+ };
203
212
  const workspaceTaskAction = (action) => async (jobId, taskId) => {
204
213
  const overview = cachedOverviews.find(item => item.job.id === jobId);
205
214
  if (!overview)
@@ -255,6 +264,7 @@ export async function runDaemon(argv) {
255
264
  start: workspaceJobAction('start'),
256
265
  reconcile: workspaceJobAction('reconcile'),
257
266
  cancel: workspaceJobAction('cancel'),
267
+ jobSetState: workspaceJobSetState,
258
268
  taskRetry: workspaceTaskAction('retry'),
259
269
  taskCancel: workspaceTaskAction('cancel'),
260
270
  taskSetState: workspaceTaskSetState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.159",
3
+ "version": "1.0.160",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {