agents-relay 2.0.140 → 2.0.141

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.
@@ -1,21 +1,39 @@
1
1
  import { eligibleWorkerCandidates, workerCandidates } from './worker-routing.js';
2
2
  import { routeExecution } from './execution-routing.js';
3
+ import { eventFor } from './events.js';
4
+ export function codexFallbackRouting(task, adapters, registered, usage) {
5
+ const requirements = task.workerRequirements ?? { capabilities: [] };
6
+ const excludedWorkers = new Set((task.routingExclusions ?? []).map(item => item.workerId).filter((id) => Boolean(id)));
7
+ const excludedAdapters = new Set((task.routingExclusions ?? []).map(item => item.adapter).filter((adapter) => Boolean(adapter)));
8
+ const candidates = eligibleWorkerCandidates(workerCandidates(adapters, registered, usage), requirements, { strictCapabilities: true })
9
+ .filter(candidate => candidate.adapter === 'codex' && !excludedWorkers.has(candidate.workerId) && !excludedAdapters.has(candidate.adapter));
10
+ const excludedRoutes = (task.routingExclusions ?? []).map(item => ({ provider: item.provider, model: item.model, ...(item.profile ? { profile: item.profile } : {}) }));
11
+ return { candidates, excludedRoutes };
12
+ }
3
13
  export async function codexFallbackCompatible(task, adapters, registered, usage) {
4
14
  if (task.adapter !== 'chatgpt' || task.chatgptThreadDelivery || task.workerRequirements?.runtime === 'chatgpt')
5
15
  return false;
6
16
  const requirements = task.workerRequirements ?? { capabilities: [] };
7
- const candidates = eligibleWorkerCandidates(workerCandidates(adapters, registered, usage), requirements, { strictCapabilities: true })
8
- .filter(candidate => candidate.adapter === 'codex');
17
+ const { candidates, excludedRoutes } = codexFallbackRouting(task, adapters, registered, usage);
9
18
  if (candidates.length === 0)
10
19
  return false;
11
20
  try {
12
- await routeExecution(candidates, requirements, { registered, usage, taskFacts: { task } });
21
+ await routeExecution(candidates, requirements, { registered, usage, taskFacts: { task }, excludedRoutes });
13
22
  return true;
14
23
  }
15
24
  catch {
16
25
  return false;
17
26
  }
18
27
  }
28
+ export async function quarantineUnavailableCodexFallback(transitions, emit, jobId, taskId, error) {
29
+ const transition = await transitions.apply(jobId, taskId, 'worker.platform_failure', {
30
+ eligible: (_job, task) => task.state === 'QUEUED' && task.adapter === 'chatgpt' && task.platformFailure?.safeToReroute === true,
31
+ facts: () => ({ platformFallbackSafe: false }),
32
+ mutate: (_job, task) => { task.error = `Codex fallback route became unavailable: ${error instanceof Error ? error.message : String(error)}`; },
33
+ });
34
+ if (transition?.decision.accepted)
35
+ await emit(eventFor(jobId, taskId, transition.task.parentTaskId, 'task.blocked', 'blocked', transition.task.error ?? 'Codex fallback route unavailable', 'user', { reason: transition.task.failureReason, phase: 'fallback-routing' }));
36
+ }
19
37
  export function persistChatGptPlatformFailure(task, original, executionId, classification, evidence, safeToReroute) {
20
38
  const route = original?.routing;
21
39
  const workerId = original?.workerSelection?.workerId ?? 'chatgpt';
@@ -11,6 +11,7 @@ import { createTaskEntity } from './workflow/entity-creation.js';
11
11
  import { localSourceHead, localWorkingHead } from './local-git-merge.js';
12
12
  import { chatgptThreadRequirements, resolveReviewerThreadDestination, resolveImplementationTaskThreadDestination, prepareReviewerThreadDelivery } from './reviewer-thread.js';
13
13
  import { reviewGenerationMatches } from './review-generation-fence.js';
14
+ import { codexFallbackRouting, quarantineUnavailableCodexFallback } from './chatgpt-platform-fallback.js';
14
15
  export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
15
16
  async currentHeadSha(job) {
16
17
  // GitHub PR head facts are audit-only. Source identity is local Git.
@@ -268,9 +269,11 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
268
269
  return { task, routed: false };
269
270
  const usage = this.options.usageSnapshotProvider ? await this.options.usageSnapshotProvider() : undefined;
270
271
  const registered = await this.store.listAgents();
272
+ const codexFallback = task.platformFailure?.safeToReroute === true && task.adapter === 'chatgpt';
273
+ const fallbackRouting = codexFallback ? codexFallbackRouting(task, this.options.adapters, registered, usage) : undefined;
271
274
  const excludedWorkers = new Set((task.routingExclusions ?? []).map(item => item.workerId).filter((id) => Boolean(id)));
272
275
  const excludedAdapters = new Set((task.routingExclusions ?? []).map(item => item.adapter).filter((adapter) => Boolean(adapter)));
273
- const candidates = workerCandidates(this.options.adapters, registered, usage)
276
+ const candidates = fallbackRouting?.candidates ?? workerCandidates(this.options.adapters, registered, usage)
274
277
  .filter(candidate => !excludedWorkers.has(candidate.workerId))
275
278
  .filter(candidate => !excludedAdapters.has(candidate.adapter))
276
279
  .filter(candidate => (implementationThread || reviewerThread) ? candidate.adapter === 'chatgpt' : true);
@@ -301,7 +304,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
301
304
  const parentExcludedRoutes = parentRouteFailedBeforeStart && parent?.routing
302
305
  ? [{ provider: parent.routing.provider, model: parent.routing.model, profile: parent.routing.profile }]
303
306
  : [];
304
- const taskExcludedRoutes = (task.routingExclusions ?? []).map(item => ({ provider: item.provider, model: item.model, profile: item.profile }));
307
+ const taskExcludedRoutes = fallbackRouting?.excludedRoutes ?? (task.routingExclusions ?? []).map(item => ({ provider: item.provider, model: item.model, profile: item.profile }));
305
308
  const excludedRoutes = [...taskExcludedRoutes, ...parentExcludedRoutes];
306
309
  const routed = await routeExecution(candidates, requirements, {
307
310
  decisionEngine: this.options.decisionEngine,
@@ -325,6 +328,17 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
325
328
  await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'model.selected', 'queued', 'Selected ' + task.routing.model + ' · thinking ' + (task.routing.reasoning ?? 'unknown'), 'user', { routing: task.routing }, { agent: 'relay', component: 'agents-relay' }));
326
329
  return { task, routed: true };
327
330
  }
331
+ async routeTaskForExecutionFailClosed(job, task) {
332
+ try {
333
+ return await this.routeTaskForExecution(job, task);
334
+ }
335
+ catch (error) {
336
+ if (task.adapter !== 'chatgpt' || !task.platformFailure?.safeToReroute)
337
+ throw error;
338
+ await quarantineUnavailableCodexFallback(this.taskTransitions, event => this.emit(event), job.id, task.id, error);
339
+ return undefined;
340
+ }
341
+ }
328
342
  async applyReviewEvent(event) {
329
343
  const job = await this.store.load(event.job_id);
330
344
  const task = job.tasks.find(item => item.id === event.task_id && item.agentName === 'reviewer');
@@ -374,7 +374,10 @@ export class Reconciler extends ReconcilerReviewBase {
374
374
  await bindPlannedReviewHead(this.store, freshTask, await this.currentHeadSha(freshJob));
375
375
  const implementationThread = resolveImplementationTaskThreadDestination(freshJob, freshTask);
376
376
  if ((!freshTask.routing || ((implementationThread || (freshTask.agentName === 'reviewer' && resolveReviewerThreadDestination(freshJob, this.options.reviewer?.threadId))) && freshTask.adapter !== 'chatgpt')) && freshTask.adapter !== 'orchestrator') {
377
- freshTask = (await this.routeTaskForExecution(freshJob, freshTask)).task;
377
+ const routed = await this.routeTaskForExecutionFailClosed(freshJob, freshTask);
378
+ if (!routed)
379
+ return;
380
+ freshTask = routed.task;
378
381
  }
379
382
  if (['codex', 'chatgpt'].includes(freshTask.adapter) && !freshTask.routing) {
380
383
  throw new Error('Task ' + freshTask.id + ' requires routing metadata before execution claim');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.140",
3
+ "version": "2.0.141",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "repository": {
6
6
  "type": "git",