agents-relay 1.0.74 → 1.0.75

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
@@ -673,6 +673,32 @@ async function runJobCommand(action, args) {
673
673
  export function runtimePlanner(args) { const plannerCommand = arg([...args], '--planner-command'); const modelRuntime = arg([...args], '--codex', 'codex'); return plannerCommand ? new CommandObjectivePlanner(plannerCommand) : new AgentObjectivePlanner(modelRuntime); }
674
674
  export function runtimeReviewer() { return { requirements: { capabilities: ['model'] } }; }
675
675
  export function runtime(store, args, bus, usageRegistry = codexAndZaiUsageRegistry()) { const emit = (event) => { process.stderr.write(`${JSON.stringify(event)}\n`); }; const modelRuntime = arg(args, '--codex', 'codex'); const policy = chatGptTabClosePolicy(args); const planner = runtimePlanner(args); return new Reconciler(store, { owner: arg(args, '--owner', `cli-${process.pid}`), maxConcurrent: Number(arg(args, '--concurrency', '4')), leaseMs: Number(arg(args, '--lease-ms', '300000')), adapters: [new CodexAdapter(modelRuntime), new ChatGptAdapter({ tabClosePolicy: policy })], continuations: [new CodexThreadContinuation(modelRuntime), new CommandContinuation(), new WebhookContinuation()], planner, usageSnapshotProvider: () => usageRegistry.snapshot(), reviewer: runtimeReviewer(), eventBus: bus, emit, chatgptTabClosePolicy: policy, projectPathForJob: job => resolveManagedProjectPath(job.repository, arg(args, '--project-path'), arg(args, '--workspace-root') || defaultWorkspaceRoot()) }); }
676
+ export function dashboardTaskAction(store, reconciler, action) {
677
+ return async (jobId, taskId) => {
678
+ const job = await store.load(jobId);
679
+ const task = job.tasks.find(item => item.id === taskId);
680
+ if (!task)
681
+ throw new Error(`Task ${taskId} not found`);
682
+ if (action === 'retry') {
683
+ if (!['FAILED', 'BLOCKED'].includes(task.state))
684
+ throw new Error(`Task ${task.id} is not restartable (${task.state})`);
685
+ task.maxAttempts = extendRetryBudget(task);
686
+ await transitionTaskEvent(task, 'retry');
687
+ task.error = null;
688
+ }
689
+ else {
690
+ if (!['DRAFT', 'QUEUED', 'BLOCKED', 'FAILED'].includes(task.state))
691
+ throw new Error(`Task ${task.id} is not cancellable (${task.state})`);
692
+ await transitionTaskEvent(task, 'cancel');
693
+ task.error = 'Cancelled by user';
694
+ }
695
+ task.leaseOwner = null;
696
+ task.leaseExpiresAt = null;
697
+ task.executionId = null;
698
+ await store.saveTask(task);
699
+ return await reconciler.wake(job.id);
700
+ };
701
+ }
676
702
  export function dashboardJobAction(store, reconciler, action) {
677
703
  return async (jobId) => {
678
704
  const job = await store.load(jobId);
@@ -724,6 +750,8 @@ export async function createService(args) {
724
750
  start: dashboardJobAction(loaded.store, reconciler, 'start'),
725
751
  reconcile: dashboardJobAction(loaded.store, reconciler, 'reconcile'),
726
752
  cancel: dashboardJobAction(loaded.store, reconciler, 'cancel'),
753
+ taskRetry: dashboardTaskAction(loaded.store, reconciler, 'retry'),
754
+ taskCancel: dashboardTaskAction(loaded.store, reconciler, 'cancel'),
727
755
  });
728
756
  await once(dashboardServer, 'listening');
729
757
  const stopWatcher = await reconciler.watch(loaded.job.id);
package/dist/dashboard.js CHANGED
@@ -124,6 +124,7 @@ export const dashboardHtml = `<!doctype html>
124
124
  .task-row { width:100%; display:flex; align-items:center; gap:.4rem; min-width:0; padding:.32rem .38rem; border:0; border-radius:.35rem; background:transparent; color:inherit; font:inherit; text-align:left; cursor:pointer; }
125
125
  .task-row[aria-current="true"] { background:var(--accent2); }
126
126
  .task-id { min-width:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font: .77rem ui-monospace,SFMono-Regular,Menlo,monospace; }
127
+ .task-actions { display:flex; justify-content:flex-end; gap:.25rem; margin-bottom:.5rem; }
127
128
  .event-list { display:grid; gap:.45rem; }
128
129
  .event-item { padding-bottom:.4rem; border-bottom:1px solid var(--border); }
129
130
  .event-meta { display:flex; flex-wrap:wrap; gap:.4rem; color:var(--muted); font-size:.68rem; }
@@ -274,7 +275,10 @@ function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();
274
275
  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;}
275
276
  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));}
276
277
  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;}
277
- 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'),field('State',task.state),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'),chatField(task));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);}
278
+ function taskActionEnabled(task,action){if(action==='retry')return ['FAILED','BLOCKED'].includes(task.state);if(action==='cancel')return ['DRAFT','QUEUED','BLOCKED','FAILED'].includes(task.state);return false;}
279
+ function taskActionButton(task,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=!taskActionEnabled(task,action);button.addEventListener('click',()=>void runTaskAction(task,action,button));return button;}
280
+ async function runTaskAction(task,action,button){const job=selected();if(!job)return;button.disabled=true;try{const response=await fetch('/api/task-action?id='+encodeURIComponent(job.id)+'&task='+encodeURIComponent(task.id)+'&action='+encodeURIComponent(action),{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){button.title=(action==='retry'?'Restart':'Cancel')+': '+error.message;}finally{renderAll();}}
281
+ function renderTaskDetail(task){taskDetailRoot.replaceChildren();if(!task){taskDetailRoot.append(element('p','empty',text('Select a task to inspect execution details.')));return;}const actions=element('div','task-actions');actions.append(taskActionButton(task,'retry','Restart task','↻'),taskActionButton(task,'cancel','Cancel task','×'));const routing=task.routing||{};const groups=element('div','detail-groups');groups.append(field('Task',task.id),field('Kind',task.kind||'work'),field('State',task.state),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'),chatField(task));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(actions,groups);}
278
282
  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();}
279
283
  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('Review',reviewStatus(job)),field('Review head SHA',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();}
280
284
  function renderAll(){jobs.sort(compareJobs);syncFilters();renderJobList();renderSelectedJob();}
@@ -485,6 +489,27 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
485
489
  }
486
490
  return;
487
491
  }
492
+ if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/task-action') {
493
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
494
+ const requestedJobId = url.searchParams.get('id') ?? '';
495
+ const requestedTaskId = url.searchParams.get('task') ?? '';
496
+ const action = url.searchParams.get('action') ?? '';
497
+ const handler = action === 'retry' ? actions?.taskRetry : action === 'cancel' ? actions?.taskCancel : undefined;
498
+ if (req.method !== 'POST' || !/^[A-Za-z0-9_-]+$/.test(requestedJobId) || !/^[A-Za-z0-9_-]+$/.test(requestedTaskId) || !handler) {
499
+ res.writeHead(404);
500
+ res.end('Task action unavailable');
501
+ return;
502
+ }
503
+ try {
504
+ res.writeHead(200, { 'content-type': 'application/json' });
505
+ res.end(JSON.stringify(await handler(requestedJobId, requestedTaskId)));
506
+ }
507
+ catch (error) {
508
+ res.writeHead(409);
509
+ res.end(error instanceof Error ? error.message : String(error));
510
+ }
511
+ return;
512
+ }
488
513
  if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/job') {
489
514
  try {
490
515
  const requestedJobId = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams.get('id') ?? undefined;
package/dist/relayd.js CHANGED
@@ -11,7 +11,7 @@ import { RepositoryWorkerPool, aggregateManagedGitHubJobs } from './pool.js';
11
11
  import { Reconciler } from './reconciler.js';
12
12
  import { dashboardManagedJobs, GitHubStore, InMemoryStore, loadManagedGitHubPullRequestGraphql } from './store.js';
13
13
  import { githubAuthContext } from './github-auth.js';
14
- import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
14
+ import { dashboardJobAction, dashboardTaskAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
15
15
  import { codexAndZaiUsageRegistry } from './usage.js';
16
16
  import { defaultWorkspaceRoot, discoverWorkspaceRepositories, resolveManagedProjectPath } from './workspace.js';
17
17
  import { deriveJobRuntimeStatus } from './runtime-status.js';
@@ -127,6 +127,13 @@ export async function runDaemon(argv) {
127
127
  const store = new GitHubStore(client, overview.job.repository, overview.job.prNumber, trusted);
128
128
  return await dashboardJobAction(store, makeReconciler(store, concurrency), action)(jobId);
129
129
  };
130
+ const workspaceTaskAction = (action) => async (jobId, taskId) => {
131
+ const overview = cachedOverviews.find(item => item.job.id === jobId);
132
+ if (!overview)
133
+ throw new Error(`Managed job ${jobId} not found`);
134
+ const store = new GitHubStore(client, overview.job.repository, overview.job.prNumber, trusted);
135
+ return await dashboardTaskAction(store, makeReconciler(store, concurrency), action)(jobId, taskId);
136
+ };
130
137
  const secretFile = value(argv, '--github-webhook-secret-file');
131
138
  const secret = value(argv, '--github-webhook-secret') || process.env.AGENTS_RELAY_GITHUB_WEBHOOK_SECRET || (secretFile ? (await readFile(secretFile, 'utf8')).trim() : '');
132
139
  const webhook = secret ? {
@@ -170,6 +177,8 @@ export async function runDaemon(argv) {
170
177
  start: workspaceJobAction('start'),
171
178
  reconcile: workspaceJobAction('reconcile'),
172
179
  cancel: workspaceJobAction('cancel'),
180
+ taskRetry: workspaceTaskAction('retry'),
181
+ taskCancel: workspaceTaskAction('cancel'),
173
182
  });
174
183
  await once(server, 'listening');
175
184
  const reconcileSafely = async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.74",
3
+ "version": "1.0.75",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {