agents-relay 1.0.171 → 1.0.173

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/pool.js CHANGED
@@ -176,7 +176,18 @@ export class RepositoryWorkerPool {
176
176
  capacity -= allowance;
177
177
  }
178
178
  }
179
- async reconcileKnown(discovered) { await this.reconcileDiscovered(discovered); }
179
+ async reconcileKnown(discovered) {
180
+ for (const item of discovered.filter(value => value.githubState === 'OPEN' && value.job.state === 'ACTIVE')) {
181
+ this.jobTargets.set(item.job.id, { repository: item.job.repository, prNumber: item.job.prNumber });
182
+ const store = new GitHubStore(this.client, item.job.repository, item.job.prNumber, this.trustedAuthors);
183
+ const reconciler = this.makeReconciler(store, 0);
184
+ if (typeof reconciler.recoverStartupOrphans === 'function')
185
+ await this.reconcileExclusive(item.job.id, async () => {
186
+ await withGitHubRequestContext(this.client, 'pool.startup-orphan-recovery', () => reconciler.recoverStartupOrphans(item.job.id));
187
+ });
188
+ }
189
+ await this.reconcileDiscovered(discovered);
190
+ }
180
191
  async reconcile() {
181
192
  if (this.isRateLimited())
182
193
  return;
@@ -104,7 +104,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
104
104
  const registered = await this.store.listAgents();
105
105
  const candidates = workerCandidates(this.options.adapters, registered, usage);
106
106
  const runtimeSummary = this.options.runtimeSummaryProvider?.();
107
- 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'))) {
107
+ for (const task of job.tasks.filter(item => item.kind !== 'planner' && item.adapter !== 'orchestrator' && ['QUEUED', 'BLOCKED'].includes(item.state) && item.output && !item.workerSelection && (item.workerRequirements || item.planningSource === 'model-planner'))) {
108
108
  const requirements = task.workerRequirements ?? { capabilities: [] };
109
109
  const taskFacts = {
110
110
  id: task.id,
@@ -214,6 +214,8 @@ export class Reconciler extends ReconcilerReviewBase {
214
214
  this.reconciling = false;
215
215
  }
216
216
  }
217
+ async recoverStartupOrphans(jobId) { const job = await this.store.load(jobId); if (job.state === 'ACTIVE' && (await this.store.pullRequestState?.() ?? 'OPEN') === 'OPEN')
218
+ await this.recoverOrphanedLocalExecutions(job); return this.store.load(jobId); }
217
219
  async cancelActiveTasks(jobId, reason) {
218
220
  const job = await this.store.load(jobId);
219
221
  for (const task of job.tasks.filter(item => !['SUCCEEDED', 'FAILED', 'CANCELLED'].includes(item.state))) {
@@ -262,6 +264,26 @@ export class Reconciler extends ReconcilerReviewBase {
262
264
  await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'task.failed', 'failed', task.error, 'user', { reason: 'lease_expired' }));
263
265
  }
264
266
  }
267
+ async recoverOrphanedLocalExecutions(job) {
268
+ for (const stale of job.tasks.filter(t => t.state === 'RUNNING' && t.executionId && t.leaseOwner && t.leaseOwner !== this.options.owner && t.adapter !== 'chatgpt')) {
269
+ const task = (await this.store.load(job.id)).tasks.find(t => t.id === stale.id);
270
+ if (!task || task.state !== 'RUNNING' || task.executionId !== stale.executionId || task.leaseOwner !== stale.leaseOwner || task.leaseExpiresAt !== stale.leaseExpiresAt || task.adapter === 'chatgpt' || task.leaseOwner === this.options.owner)
271
+ continue;
272
+ await transitionTaskEvent(task, 'task.failed');
273
+ task.error = 'Orphaned after relayd restart';
274
+ task.leaseOwner = null;
275
+ task.leaseExpiresAt = null;
276
+ task.executionId = null;
277
+ if (task.attempt < task.maxAttempts) {
278
+ await transitionTaskEvent(task, 'retry');
279
+ await this.store.saveTask(task);
280
+ continue;
281
+ }
282
+ task.error += ' after maximum attempts';
283
+ await this.store.saveTask(task);
284
+ await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'task.failed', 'failed', task.error, 'user', { reason: 'startup_orphaned' }));
285
+ }
286
+ }
265
287
  async planJob(jobId, guidance) {
266
288
  return executeManualPlan(this.store, jobId, guidance, this.options.plannerTimeoutMs ?? storySizeTimeoutMs('M'), (job, task) => this.launchPlanner(job, task));
267
289
  }
package/dist/relayd.js CHANGED
@@ -120,7 +120,7 @@ export async function runDaemon(argv) {
120
120
  cachedOverviews = parsed;
121
121
  }
122
122
  catch { /* first run or invalid cache */ }
123
- const refreshAll = async (full = false) => {
123
+ const refreshAll = async (full = false, failOnError = false) => {
124
124
  if (client.rateLimitStatus().limited)
125
125
  return cachedOverviews;
126
126
  const scanRepositories = full ? repositories : recoveryScanRepositories(repositories, cachedOverviews, dormantProbeIndex++);
@@ -139,6 +139,8 @@ export async function runDaemon(argv) {
139
139
  return cachedOverviews;
140
140
  }
141
141
  catch (error) {
142
+ if (failOnError)
143
+ throw error;
142
144
  process.stderr.write(`warning: workspace job refresh unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
143
145
  return cachedOverviews;
144
146
  }
@@ -275,7 +277,6 @@ export async function runDaemon(argv) {
275
277
  taskCancel: workspaceTaskAction('cancel'),
276
278
  taskSetState: workspaceTaskSetState,
277
279
  });
278
- await once(server, 'listening');
279
280
  const reconcileSafely = async () => {
280
281
  if (client.rateLimitStatus().limited)
281
282
  return;
@@ -301,17 +302,23 @@ export async function runDaemon(argv) {
301
302
  }
302
303
  };
303
304
  const watchdogDefault = webhook ? SERVICE_DEFAULTS.webhookWatchdogMs : SERVICE_DEFAULTS.watchdogMs;
304
- const interval = setInterval(() => void reconcileSafely(), Number(value(argv, '--interval', String(watchdogDefault))));
305
- interval.unref();
306
- const scheduleInterval = setInterval(() => void scheduleHeartbeatSafely(), SCHEDULE_HEARTBEAT_MS);
307
- scheduleInterval.unref();
308
- const stop = () => { clearInterval(interval); clearInterval(scheduleInterval); void stopWatch().finally(() => server.close(() => process.exit(0))); };
305
+ let interval;
306
+ let scheduleInterval;
307
+ const stop = () => { if (interval)
308
+ clearInterval(interval); if (scheduleInterval)
309
+ clearInterval(scheduleInterval); void stopWatch().finally(() => server.close(() => process.exit(0))); };
309
310
  process.once('SIGINT', stop);
310
311
  process.once('SIGTERM', stop);
312
+ await once(server, 'listening');
313
+ const startupItems = await refreshAll(true, true); // void refreshAll(true)
314
+ await pool.reconcileKnown(startupItems);
315
+ // Startup recovery must finish before normal polling or scheduled work can run.
316
+ await scheduleHeartbeatSafely();
317
+ interval = setInterval(() => void reconcileSafely(), Number(value(argv, '--interval', String(watchdogDefault))));
318
+ interval.unref();
319
+ scheduleInterval = setInterval(() => void scheduleHeartbeatSafely(), SCHEDULE_HEARTBEAT_MS);
320
+ scheduleInterval.unref();
311
321
  console.log('Dashboard listening; repository worker pool active');
312
- void refreshAll(true)
313
- .then(async (items) => { await scheduleHeartbeatSafely(); await pool.reconcileKnown(items); })
314
- .catch(error => process.stderr.write(`warning: startup reconciliation unavailable: ${error instanceof Error ? error.message : String(error)}\n`));
315
322
  }
316
323
  if (isEntrypoint(import.meta.url))
317
324
  runDaemon(process.argv.slice(2)).catch(error => { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.171",
3
+ "version": "1.0.173",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {