agents-relay 1.0.52 → 1.0.54

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
@@ -411,7 +411,7 @@ async function runTaskCommand(action, args) {
411
411
  if (action === 'retry' || action === 'cancel') {
412
412
  if (action === 'retry')
413
413
  task.maxAttempts = extendRetryBudget(task);
414
- task.state = action === 'retry' ? 'READY' : 'CANCELLED';
414
+ task.state = action === 'retry' ? 'QUEUED' : 'CANCELLED';
415
415
  task.error = action === 'retry' ? null : 'Cancelled by user';
416
416
  task.updatedAt = new Date().toISOString();
417
417
  await store.saveTask(task);
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { storySizeTimeoutMs, taskOverallTimeoutMs, chatGptStartAckTimeoutMs } from './types.js';
3
- import { schedule, transitionTask } from './scheduler.js';
3
+ import { dependencyReady, schedule, transitionTask } from './scheduler.js';
4
4
  import { LaunchFailure, StartAckTimeout } from './adapters.js';
5
5
  import { eventFor } from './events.js';
6
6
  import { parsePlannerResult, plannerInput } from './planner.js';
@@ -160,7 +160,7 @@ export class Reconciler {
160
160
  for (const task of scheduled.tasks.filter(item => item.state === 'SUCCEEDED' && (item.continuation || scheduled.continuation) && !item.continuationDeliveredAt))
161
161
  this.detach(this.deliverContinuation(scheduled, task), scheduled.id, task.id, 'continuation');
162
162
  const capacity = Math.max(0, (this.options.maxConcurrent ?? 4) - this.live.size - this.plannerLive.size);
163
- for (const task of scheduled.tasks.filter(t => t.state === 'READY').sort((a, b) => compareTaskPriority(a, b, scheduled.priority ?? 'P2')).slice(0, capacity)) {
163
+ for (const task of scheduled.tasks.filter(t => t.state === 'QUEUED' && dependencyReady(t, new Map(scheduled.tasks.map(item => [item.id, item])))).sort((a, b) => compareTaskPriority(a, b, scheduled.priority ?? 'P2')).slice(0, capacity)) {
164
164
  if (task.kind === 'planner')
165
165
  await this.launchPlanner(scheduled, task);
166
166
  else if (task.adapter === 'orchestrator') {
@@ -199,7 +199,7 @@ export class Reconciler {
199
199
  return;
200
200
  job.review = { ...job.review, outcome: 'stale', updatedAt: new Date().toISOString() };
201
201
  const staleTask = job.review.taskId ? job.tasks.find(task => task.id === job.review?.taskId) : undefined;
202
- if (staleTask && ['QUEUED', 'READY', 'RUNNING', 'WAITING'].includes(staleTask.state)) {
202
+ if (staleTask && ['QUEUED', 'RUNNING', 'WAITING'].includes(staleTask.state)) {
203
203
  this.live.get(staleTask.id)?.execution.cancel();
204
204
  transitionTask(staleTask, 'CANCELLED');
205
205
  staleTask.error = 'Review evidence became stale after the PR head changed';
@@ -218,7 +218,7 @@ export class Reconciler {
218
218
  if (review?.headSha === headSha && ['started', 'approved', 'changes_required', 'blocked'].includes(review.outcome))
219
219
  return;
220
220
  const reviewTasks = job.tasks.filter(task => task.agentName === 'reviewer' && task.reviewHeadSha === headSha);
221
- if (reviewTasks.some(task => ['QUEUED', 'READY', 'RUNNING', 'WAITING'].includes(task.state)))
221
+ if (reviewTasks.some(task => ['QUEUED', 'RUNNING', 'WAITING'].includes(task.state)))
222
222
  return;
223
223
  if (reviewTasks.some(task => task.state === 'SUCCEEDED' && review?.headSha === headSha && review.outcome === 'approved'))
224
224
  return;
@@ -235,7 +235,7 @@ export class Reconciler {
235
235
  await this.emit(eventFor(job.id, id, null, 'review.created', 'queued', `Final review task ${id} created for ${headSha} using ${reviewerRouting.reason}`, 'orchestrator', { headSha, agentName: 'reviewer', reviewerRouting: { workerId: reviewerRouting.selection.workerId, reason: reviewerRouting.reason, selectedBy: reviewerRouting.selection.selectedBy } }));
236
236
  const ready = schedule(await this.store.load(job.id));
237
237
  await this.store.saveJob(ready);
238
- const reviewTask = ready.tasks.find(item => item.id === id && item.state === 'READY');
238
+ const reviewTask = ready.tasks.find(item => item.id === id && item.state === 'QUEUED');
239
239
  if (reviewTask && (this.options.maxConcurrent ?? 4) > this.live.size + this.plannerLive.size)
240
240
  await this.launch(ready, reviewTask);
241
241
  }
@@ -270,7 +270,7 @@ export class Reconciler {
270
270
  async routeExecutableTasks(job) {
271
271
  const usage = this.options.usageSnapshotProvider ? await this.options.usageSnapshotProvider() : undefined;
272
272
  const candidates = workerCandidates(this.options.adapters, await this.store.listAgents(), usage);
273
- for (const task of job.tasks.filter(item => item.kind !== 'planner' && item.agentName !== 'reviewer' && item.adapter !== 'orchestrator' && ['QUEUED', 'READY', 'BLOCKED'].includes(item.state) && item.output && !item.workerSelection && (item.workerRequirements || item.planningSource === 'model-planner'))) {
273
+ for (const task of job.tasks.filter(item => item.kind !== 'planner' && item.agentName !== 'reviewer' && item.adapter !== 'orchestrator' && ['QUEUED', 'BLOCKED'].includes(item.state) && item.output && !item.workerSelection && (item.workerRequirements || item.planningSource === 'model-planner'))) {
274
274
  const requirements = task.workerRequirements ?? { capabilities: task.capabilities.length > 0 ? task.capabilities : ['model'] };
275
275
  const routed = selectWorker(candidates, requirements);
276
276
  task.workerSelection = routed.selection;
@@ -365,7 +365,7 @@ export class Reconciler {
365
365
  this.plannerLive.delete(task.id);
366
366
  }
367
367
  if (task.attempt < task.maxAttempts) {
368
- transitionTask(task, 'READY');
368
+ transitionTask(task, 'QUEUED');
369
369
  task.leaseOwner = null;
370
370
  task.leaseExpiresAt = null;
371
371
  await this.store.saveTask(task);
@@ -391,7 +391,7 @@ export class Reconciler {
391
391
  const slot = job.schedule?.currentRunSlot ? Date.parse(job.schedule.currentRunSlot) : Number.NaN;
392
392
  const scopedTasks = Number.isFinite(slot) ? job.tasks.filter(task => Date.parse(task.createdAt) >= slot) : job.tasks;
393
393
  const planners = scopedTasks.filter(task => task.kind === 'planner').sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
394
- if (planners.some(task => ['QUEUED', 'READY', 'RUNNING', 'WAITING'].includes(task.state)))
394
+ if (planners.some(task => ['QUEUED', 'RUNNING', 'WAITING'].includes(task.state)))
395
395
  return job;
396
396
  if (scopedTasks.some(task => task.state === 'FAILED' || task.state === 'BLOCKED'))
397
397
  return job;
@@ -598,7 +598,7 @@ export class Reconciler {
598
598
  task.error = error.message;
599
599
  task.leaseOwner = null;
600
600
  task.leaseExpiresAt = null;
601
- transitionTask(task, task.attempt < task.maxAttempts ? 'READY' : 'FAILED');
601
+ transitionTask(task, task.attempt < task.maxAttempts ? 'QUEUED' : 'FAILED');
602
602
  await this.store.saveTask(task);
603
603
  await this.emit(eventFor(job.id, task.id, task.parentTaskId, task.state === 'FAILED' ? 'planner.failed' : 'planner.retry', task.state === 'FAILED' ? 'failed' : 'queued', `Planner ${task.id}: ${error.message}`, 'user'));
604
604
  this.detach(this.reconcile(jobId), jobId, task.id, 'planner retry reconciliation');
@@ -636,7 +636,7 @@ export class Reconciler {
636
636
  task.error = message;
637
637
  task.leaseOwner = null;
638
638
  task.leaseExpiresAt = null;
639
- transitionTask(task, task.attempt < task.maxAttempts ? 'READY' : 'FAILED');
639
+ transitionTask(task, task.attempt < task.maxAttempts ? 'QUEUED' : 'FAILED');
640
640
  await this.store.saveTask(task);
641
641
  await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'planner.failed', 'failed', `Planner ${task.id}: ${message}`, 'user'));
642
642
  }
package/dist/scheduler.js CHANGED
@@ -33,11 +33,11 @@ export function schedule(job) {
33
33
  const map = new Map(job.tasks.map(t => [t.id, t]));
34
34
  for (const task of scopedTasks) {
35
35
  if (['QUEUED', 'WAITING'].includes(task.state) && dependencyReady(task, map))
36
- transitionTask(task, 'READY');
36
+ transitionTask(task, 'QUEUED');
37
37
  }
38
- const active = scopedTasks.some(t => ['READY', 'RUNNING'].includes(t.state));
38
+ const active = scopedTasks.some(t => ['QUEUED', 'RUNNING'].includes(t.state));
39
39
  const blocked = scopedTasks.some(t => t.state === 'BLOCKED');
40
- const waiting = scopedTasks.some(t => ['QUEUED', 'WAITING'].includes(t.state));
40
+ const waiting = scopedTasks.some(t => t.state === 'WAITING');
41
41
  const failed = scopedTasks.some(t => t.state === 'FAILED');
42
42
  const cancelled = scopedTasks.every(t => t.state === 'CANCELLED');
43
43
  const next = active ? 'RUNNING' : failed ? 'FAILED' : blocked ? 'BLOCKED' : waiting ? 'WAITING' : cancelled ? 'CANCELLED' : completionGateSatisfied(job) ? 'COMPLETED' : 'WAITING';
@@ -65,7 +65,7 @@ export function extendRetryBudget(task, windowSize = 3) {
65
65
  const usedOrBudgeted = Math.max(task.maxAttempts, task.attempt);
66
66
  return Math.max(windowSize, (Math.floor(usedOrBudgeted / windowSize) + 1) * windowSize);
67
67
  }
68
- export function transitionTask(task, state) { assertTaskTransition(task.state, state); task.state = state; if (['READY', 'RUNNING', 'SUCCEEDED'].includes(state))
68
+ export function transitionTask(task, state) { assertTaskTransition(task.state, state); task.state = state; if (['QUEUED', 'RUNNING', 'SUCCEEDED'].includes(state))
69
69
  task.error = null; task.updatedAt = new Date().toISOString(); }
70
70
  export const SCHEDULE_RESOLUTION_MS = 600_000;
71
71
  export const RUN_BUCKET_LABEL_COUNT = 144;
package/dist/types.js CHANGED
@@ -3,7 +3,7 @@ export function storySizeTimeoutMs(size) { return STORY_SIZE_TIMEOUT_MS[size]; }
3
3
  export function taskOverallTimeoutMs(size, reasoning) { return storySizeTimeoutMs(size) * (reasoning?.toLowerCase() === 'high' ? 2 : 1); }
4
4
  export function chatGptStartAckTimeoutMs(overallTimeoutMs) { return Math.min(600000, Math.max(120000, Math.floor(overallTimeoutMs / 3))); }
5
5
  const jobTransitions = { OPEN: ['RUNNING', 'WAITING', 'BLOCKED', 'COMPLETED', 'CANCELLED'], RUNNING: ['OPEN', 'WAITING', 'BLOCKED', 'COMPLETED', 'FAILED', 'CANCELLED'], WAITING: ['RUNNING', 'BLOCKED', 'COMPLETED', 'CANCELLED'], BLOCKED: ['RUNNING', 'COMPLETED', 'CANCELLED'], COMPLETED: ['RUNNING', 'CANCELLED'], FAILED: ['RUNNING', 'COMPLETED', 'CANCELLED'], CANCELLED: ['RUNNING', 'COMPLETED'] };
6
- const taskTransitions = { QUEUED: ['READY', 'BLOCKED', 'CANCELLED'], READY: ['RUNNING', 'BLOCKED', 'FAILED', 'CANCELLED'], RUNNING: ['WAITING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED', 'READY'], WAITING: ['READY', 'BLOCKED', 'CANCELLED'], BLOCKED: ['READY', 'CANCELLED'], SUCCEEDED: [], FAILED: ['READY', 'RUNNING', 'CANCELLED'], CANCELLED: [] };
6
+ const taskTransitions = { QUEUED: ['RUNNING', 'BLOCKED', 'FAILED', 'CANCELLED'], RUNNING: ['WAITING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED', 'QUEUED'], WAITING: ['QUEUED', 'BLOCKED', 'CANCELLED'], BLOCKED: ['QUEUED', 'CANCELLED'], SUCCEEDED: [], FAILED: ['QUEUED', 'RUNNING', 'CANCELLED'], CANCELLED: [] };
7
7
  export function assertJobTransition(from, to) { if (from !== to && !jobTransitions[from].includes(to))
8
8
  throw new Error(`Invalid job transition ${from} -> ${to}`); }
9
9
  export function assertTaskTransition(from, to) { if (from !== to && !taskTransitions[from].includes(to))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.52",
3
+ "version": "1.0.54",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {