agents-relay 1.0.164 → 1.0.165

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
@@ -18,7 +18,7 @@ import { decisionMode, LayaCliDecisionEngine } from './decision.js';
18
18
  import { discoverAgents } from './registry.js';
19
19
  import { AgentObjectivePlanner, CommandObjectivePlanner } from './planner.js';
20
20
  import { extendRetryBudget, transitionJobEvent, transitionTaskEvent } from './scheduler.js';
21
- import { dueBuckets, processDueScheduledJobs, syncScheduleLabels } from './scheduled-jobs.js';
21
+ import { assertScheduledAutonomousRepositoryCapability, dueBuckets, processDueScheduledJobs, syncScheduleLabels } from './scheduled-jobs.js';
22
22
  import { resolveExecutionContext } from './execution-context.js';
23
23
  import { defaultWorkspaceRoot, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
24
24
  import { runBlockerCommand } from './cli-blocker.js';
@@ -35,6 +35,7 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
35
35
  const store = new GitHubStore(client, repositoryName, pr.number, trustedAuthors);
36
36
  try {
37
37
  const existing = await store.load(options.id);
38
+ await assertScheduledAutonomousRepositoryCapability(client, repositoryName, existing);
38
39
  if (existing.schedule)
39
40
  await syncScheduleLabels(client, repositoryName, existing);
40
41
  return { job: existing, pr };
@@ -65,6 +66,7 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
65
66
  };
66
67
  if (options.schedule)
67
68
  job.schedule = options.schedule;
69
+ await assertScheduledAutonomousRepositoryCapability(client, repositoryName, job);
68
70
  await store.saveJob(job);
69
71
  if (job.schedule)
70
72
  await syncScheduleLabels(client, repositoryName, job);
@@ -448,6 +450,7 @@ async function runJobCommand(action, args) {
448
450
  throw new Error('--continue-kind and --continue-target must be supplied together');
449
451
  job.continuation = next;
450
452
  }
453
+ await assertScheduledAutonomousRepositoryCapability(auth.client, repo, job);
451
454
  job.updatedAt = new Date().toISOString();
452
455
  await store.saveJob(job);
453
456
  if (hasArg(args, '--schedule') && job.schedule)
@@ -228,9 +228,17 @@ export class Reconciler extends ReconcilerReviewBase {
228
228
  }
229
229
  }
230
230
  async recoverLostExecutions(job, now) {
231
- for (const task of job.tasks.filter(item => item.state === 'RUNNING' || (item.state === 'QUEUED' && item.executionId !== null))) {
232
- const expired = task.leaseExpiresAt !== null && new Date(task.leaseExpiresAt).getTime() <= now;
233
- if (!expired)
231
+ const candidates = job.tasks.filter(item => item.state === 'RUNNING' || (item.state === 'QUEUED' && item.executionId !== null));
232
+ for (const staleTask of candidates) {
233
+ if (staleTask.leaseExpiresAt === null || new Date(staleTask.leaseExpiresAt).getTime() > now)
234
+ continue;
235
+ // Re-read durable state before applying lease recovery: another reconciler may have accepted a terminal event or launched a newer execution.
236
+ const task = (await this.store.load(job.id)).tasks.find(item => item.id === staleTask.id);
237
+ if (!task)
238
+ continue;
239
+ const sameGeneration = task.executionId === staleTask.executionId && task.leaseOwner === staleTask.leaseOwner && task.leaseExpiresAt === staleTask.leaseExpiresAt;
240
+ const recoverable = task.state === 'RUNNING' || (task.state === 'QUEUED' && task.executionId !== null);
241
+ if (!sameGeneration || !recoverable || task.leaseExpiresAt === null || new Date(task.leaseExpiresAt).getTime() > now)
234
242
  continue;
235
243
  this.live.get(task.id)?.execution.cancel();
236
244
  const planner = this.plannerLive.get(task.id);
@@ -239,8 +247,7 @@ export class Reconciler extends ReconcilerReviewBase {
239
247
  planner.controller.abort();
240
248
  this.plannerLive.delete(task.id);
241
249
  }
242
- const failedEvent = task.state === 'RUNNING' ? 'task.failed' : 'fail';
243
- await transitionTaskEvent(task, failedEvent);
250
+ await transitionTaskEvent(task, task.state === 'RUNNING' ? 'task.failed' : 'fail');
244
251
  task.error = 'Lease expired';
245
252
  task.leaseOwner = null;
246
253
  task.leaseExpiresAt = null;
@@ -248,12 +255,11 @@ export class Reconciler extends ReconcilerReviewBase {
248
255
  if (task.attempt < task.maxAttempts) {
249
256
  await transitionTaskEvent(task, 'retry');
250
257
  await this.store.saveTask(task);
258
+ continue;
251
259
  }
252
- else {
253
- task.error += ' after maximum attempts';
254
- await this.store.saveTask(task);
255
- await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'task.failed', 'failed', task.error, 'user', { reason: 'lease_expired' }));
256
- }
260
+ task.error += ' after maximum attempts';
261
+ await this.store.saveTask(task);
262
+ await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'task.failed', 'failed', task.error, 'user', { reason: 'lease_expired' }));
257
263
  }
258
264
  }
259
265
  async planJob(jobId, guidance) {
@@ -1,6 +1,7 @@
1
1
  import { GitHubStore, loadManagedGitHubPullRequestGraphql } from './store.js';
2
2
  import { bucketStart, nextScheduleRun, parseRunBucketLabel, runBucketLabel, scheduleBucket } from './scheduler.js';
3
3
  export const AUTONOMOUS_LABEL = 'agents-relay:autonomous';
4
+ export const AUTONOMOUS_SCHEDULER_WORKFLOW = '.github/workflows/autonomous-scheduler.yml';
4
5
  function searchPath(query, page) {
5
6
  return `search/issues?q=${encodeURIComponent(query)}&sort=created&order=asc&per_page=100&page=${page}`;
6
7
  }
@@ -17,6 +18,21 @@ export function indexedDueQuery(repository, bucket) {
17
18
  const label = runBucketLabel(bucket);
18
19
  return `repo:${repository} is:open is:pr label:"${AUTONOMOUS_LABEL}" label:"${label}"`;
19
20
  }
21
+ export async function assertScheduledAutonomousRepositoryCapability(client, repository, job) {
22
+ if (job.executionMode !== 'autonomous' || !job.schedule)
23
+ return;
24
+ const metadata = await client.request(`repos/${repository}`);
25
+ const defaultBranch = typeof metadata.default_branch === 'string' && metadata.default_branch ? metadata.default_branch : 'main';
26
+ try {
27
+ await client.request(`repos/${repository}/contents/${AUTONOMOUS_SCHEDULER_WORKFLOW}?ref=${encodeURIComponent(defaultBranch)}`);
28
+ }
29
+ catch (error) {
30
+ const message = error instanceof Error ? error.message : String(error);
31
+ if (!/404|Not Found/i.test(message))
32
+ throw error;
33
+ throw new Error(`SCHEDULED_AUTONOMOUS_UNAVAILABLE: ${repository} default branch ${defaultBranch} must contain ${AUTONOMOUS_SCHEDULER_WORKFLOW} before creating or enabling a scheduled autonomous Job`);
34
+ }
35
+ }
20
36
  async function issueLabels(client, repository, prNumber) {
21
37
  const labels = [];
22
38
  let page = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.164",
3
+ "version": "1.0.165",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -109,7 +109,9 @@ SHA-bound final reviewer. For example, `video-director + market + style ->
109
109
  review-chair -> unified revision plan` is a project pattern, not a hard-coded
110
110
  Relay reviewer list.
111
111
 
112
- A scheduled autonomous job adds cron/timezone/next-run and missed-run/concurrency policy to the trusted job marker, plus the `agents-relay:autonomous` label and one bounded time-of-day `agents-relay:next-run:*` index label. The GitHub heartbeat discovers only indexed due candidates, validates the marker's exact `nextRunAt`, claims the slot idempotently, advances the durable next run, and pulses `agents-relay:schedule-wake`. GitHub webhook delivery then wakes local Relay for planner/worker execution. While the schedule is active, the durable Job remains `ACTIVE`; between runs its derived runtime status is `WAITING`. Scheduled recurring Jobs do not enter one-shot final-review/auto-merge after each satisfied run; older-cycle failures are historical evidence and do not block a later slot.
112
+ A scheduled autonomous job adds cron/timezone/next-run and missed-run/concurrency policy to the trusted job marker, plus the `agents-relay:autonomous` label and one bounded time-of-day `agents-relay:next-run:*` index label. The target repository's default branch **must** contain `.github/workflows/autonomous-scheduler.yml`; this repository-level heartbeat is a required capability, not optional deployment glue. Job creation or update that would result in an autonomous Job with a schedule must fail loudly when that workflow is absent instead of persisting a schedule that can never advance. Unscheduled autonomous Jobs and fixed Jobs do not require this workflow.
113
+
114
+ The GitHub heartbeat discovers only indexed due candidates, validates the marker's exact `nextRunAt`, claims the slot idempotently, advances the durable next run, and pulses `agents-relay:schedule-wake`. GitHub webhook delivery then wakes local Relay for planner/worker execution. While the schedule is active, the durable Job remains `ACTIVE`; between runs its derived runtime status is `WAITING`. Scheduled recurring Jobs do not enter one-shot final-review/auto-merge after each satisfied run; older-cycle failures are historical evidence and do not block a later slot.
113
115
 
114
116
 
115
117
  ## Task evidence and head identity