agents-relay 2.0.81 → 2.0.82

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.
Files changed (38) hide show
  1. package/README.md +3 -3
  2. package/dist/cli-help.js +5 -8
  3. package/dist/cli-review.js +1 -1
  4. package/dist/cli-support.js +2 -22
  5. package/dist/cli-worker-task.js +7 -6
  6. package/dist/cli.js +22 -59
  7. package/dist/dashboard-actions.js +2 -1
  8. package/dist/dashboard-html.js +5 -5
  9. package/dist/dashboard.js +0 -4
  10. package/dist/github-binding-order.js +1 -1
  11. package/dist/job-pr-number-migration.js +24 -0
  12. package/dist/job-task-id-sequence.js +16 -9
  13. package/dist/legacy-orchestrator-repair.js +51 -0
  14. package/dist/local-job-registry.js +2 -3
  15. package/dist/local-merge-delivery.js +1 -1
  16. package/dist/managed-worker-input.js +3 -4
  17. package/dist/planner.js +14 -8
  18. package/dist/pool.js +3 -1
  19. package/dist/reconciler-review.js +2 -2
  20. package/dist/reconciler.js +2 -8
  21. package/dist/relay-store.js +6 -4
  22. package/dist/relayd-control.js +10 -0
  23. package/dist/relayd-dashboard-actions.js +3 -33
  24. package/dist/relayd.js +17 -20
  25. package/dist/reviewer-thread.js +1 -1
  26. package/dist/runtime-status.js +2 -1
  27. package/dist/task-actions.js +47 -42
  28. package/dist/task-title.js +5 -0
  29. package/dist/troubleshooter.js +2 -2
  30. package/dist/worker-routing.js +1 -1
  31. package/dist/workflow/entity-creation.js +11 -1
  32. package/dist/workflow/orchestrator-rescue.js +62 -18
  33. package/dist/workflow/troubleshooter-v2.js +1 -1
  34. package/dist/workspace.js +5 -4
  35. package/package.json +1 -1
  36. package/skills/agents-relay/contracts/orchestrator-workflows.md +13 -12
  37. package/workflow/orchestrator-rescue.mmd +16 -1
  38. package/workflow/troubleshooter.mmd +4 -4
@@ -4,9 +4,8 @@ import { createReviewChairExecutionPlan } from './review-chair.js';
4
4
  import { taskTagWorkerPrompt } from './task-tag.js';
5
5
  import { executionEventSource } from './reconciler-runtime.js';
6
6
  export async function managedWorkerInput(job, task, context) {
7
- const isLocalJob = job.prNumber === 0;
8
- const externalPrNumber = job.externalPrNumber && job.externalPrNumber > 0 ? job.externalPrNumber : (job.prNumber > 0 ? job.prNumber : 0);
9
- const projectedReview = isLocalJob && task.agentName === 'reviewer' && externalPrNumber > 0;
7
+ const isLocalJob = Boolean(job.localMerge?.sourceRef);
8
+ const projectedReview = isLocalJob && task.agentName === 'reviewer' && job.prNumber > 0;
10
9
  if (!['codex', 'chatgpt'].includes(task.adapter) || !job.repository)
11
10
  return task.input;
12
11
  const selectedAgent = task.agentName && context ? await agentContractPrompt(context, task.agentName) : '';
@@ -19,7 +18,7 @@ export async function managedWorkerInput(job, task, context) {
19
18
  if (!task.executionId)
20
19
  throw new Error(`Task ${task.id} requires an executionId before worker input is built`);
21
20
  const sourceId = executionEventSource(job.id, task.id, task.executionId);
22
- const prUrl = externalPrNumber > 0 ? `https://github.com/${job.repository}/pull/${externalPrNumber}` : null;
21
+ const prUrl = job.prNumber > 0 ? `https://github.com/${job.repository}/pull/${job.prNumber}` : null;
23
22
  const taskUrl = task.taskUrl ?? (isLocalJob ? `local:${job.id}/task/${task.id}` : `${prUrl}#issuecomment-${task.id}`);
24
23
  const output = task.output.kind === 'file'
25
24
  ? `file\nPath: ${task.output.path}`
package/dist/planner.js CHANGED
@@ -16,7 +16,7 @@ export function semanticTaskTree(tasks) {
16
16
  const ids = new Set(work.map(task => task.id));
17
17
  return {
18
18
  roots: work.filter(task => !task.parentTaskId || !ids.has(task.parentTaskId)).map(task => task.id),
19
- tasks: work.map(task => ({ id: task.id, parent_task_id: task.parentTaskId, state: task.state, dependencies: [...task.dependencies], planning_decision_id: task.planningDecisionId, planning_round: task.planningRound, review: task.review })),
19
+ tasks: work.map(task => ({ id: task.id, title: task.title, parent_task_id: task.parentTaskId, state: task.state, dependencies: [...task.dependencies], planning_decision_id: task.planningDecisionId, planning_round: task.planningRound, review: task.review })),
20
20
  };
21
21
  }
22
22
  function plannerTrigger(job, manual = false) {
@@ -113,14 +113,14 @@ export function materializePlannerChild(job, planner, spec, executionContext) {
113
113
  validateResolutionProposal(job, target, resolution);
114
114
  }
115
115
  const reviewerThread = spec.agentName === 'reviewer' ? prepareReviewerThreadDelivery(job, { id: spec.id, agentName: spec.agentName, reviewHeadSha }) : undefined;
116
- return createTaskEntity(job, { jobId: job.id, id: spec.id, kind: 'work', priority: spec.priority ?? job.priority, storySize: spec.storySize ?? 'M', projectName: spec.projectName, agentName: spec.agentName, agentGraph: spec.agentGraph, review: spec.review, parentTaskId: spec.parentTaskId ?? null, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], workerRequirements: reviewerThread ? chatgptThreadRequirements(spec.workerRequirements) : spec.workerRequirements, testInjection: spec.testInjection, adapter: reviewerThread ? 'chatgpt' : 'codex', chatgptThreadDelivery: reviewerThread, input: spec.input, output: spec.output, continuation: spec.continuation ?? null, continuationDeliveredAt: null, attempt: 0, maxAttempts: spec.maxAttempts ?? 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: spec.timeoutMs ?? taskOverallTimeoutMs(spec.storySize ?? 'M'), createdAt: now, updatedAt: now, planningDecisionId: decisionId, planningSource: source, planningRound: round, reviewHeadSha, resolution: resolution ? { ...resolution, state: 'proposed', owner: 'planner', updatedAt: now } : undefined });
116
+ return createTaskEntity(job, { jobId: job.id, id: spec.id, title: spec.title, kind: 'work', priority: spec.priority ?? job.priority, storySize: spec.storySize ?? 'M', projectName: spec.projectName, agentName: spec.agentName, agentGraph: spec.agentGraph, review: spec.review, parentTaskId: spec.parentTaskId ?? null, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], workerRequirements: reviewerThread ? chatgptThreadRequirements(spec.workerRequirements) : spec.workerRequirements, testInjection: spec.testInjection, adapter: reviewerThread ? 'chatgpt' : 'codex', chatgptThreadDelivery: reviewerThread, input: spec.input, output: spec.output, continuation: spec.continuation ?? null, continuationDeliveredAt: null, attempt: 0, maxAttempts: spec.maxAttempts ?? 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: spec.timeoutMs ?? taskOverallTimeoutMs(spec.storySize ?? 'M'), createdAt: now, updatedAt: now, planningDecisionId: decisionId, planningSource: source, planningRound: round, reviewHeadSha, resolution: resolution ? { ...resolution, state: 'proposed', owner: 'planner', updatedAt: now } : undefined });
117
117
  }
118
118
  export function planningSpec(task) {
119
- if (!task.output && task.adapter !== 'orchestrator')
119
+ if (!task.output)
120
120
  throw new Error(`Initial task ${task.id} requires an output contract`);
121
- if (!['codex', 'chatgpt', 'orchestrator'].includes(task.adapter))
122
- throw new Error(`Initial task ${task.id} requires a supported adapter`);
123
- return { id: task.id, parentTaskId: task.parentTaskId, input: task.input, priority: task.priority, storySize: task.storySize, projectName: task.projectName, agentName: task.agentName, agentGraph: task.agentGraph, review: task.review, dependencies: task.dependencies, capabilities: task.capabilities, workerRequirements: task.workerRequirements, testInjection: task.testInjection, adapter: task.adapter, output: task.output ?? { kind: 'task_pr' }, routing: task.routing, continuation: task.continuation, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs, resolution: task.resolution ? { ...task.resolution, state: undefined, owner: undefined, updatedAt: undefined } : undefined };
121
+ if (!['codex', 'chatgpt'].includes(task.adapter))
122
+ throw new Error(`Initial task ${task.id} requires a supported worker adapter`);
123
+ return { id: task.id, title: task.title, parentTaskId: task.parentTaskId, input: task.input, priority: task.priority, storySize: task.storySize, projectName: task.projectName, agentName: task.agentName, agentGraph: task.agentGraph, review: task.review, dependencies: task.dependencies, capabilities: task.capabilities, workerRequirements: task.workerRequirements, testInjection: task.testInjection, adapter: task.adapter, output: task.output ?? { kind: 'task_pr' }, routing: task.routing, continuation: task.continuation, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs, resolution: task.resolution ? { ...task.resolution, state: undefined, owner: undefined, updatedAt: undefined } : undefined };
124
124
  }
125
125
  function record(value, field = 'result') {
126
126
  if (!value || typeof value !== 'object' || Array.isArray(value))
@@ -309,7 +309,7 @@ export function parsePlannerResult(value) {
309
309
  if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs < 0)
310
310
  throw new Error(`Planner next_tasks[${index}].timeoutMs is invalid`);
311
311
  return {
312
- id, parentTaskId, input: task.input, priority, storySize,
312
+ id, title: optionalString(task.title, `next_tasks[${index}].title`), parentTaskId, input: task.input, priority, storySize,
313
313
  projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'), agentGraph, review,
314
314
  dependencies: stringArray(task.dependencies, 'dependencies'), capabilities: stringArray(task.capabilities, 'capabilities'), workerRequirements: requirements,
315
315
  testInjection: parseTestInjection(task.testInjection ?? task['--test-injection']) ?? optionalString(task.testInjection ?? task['--test-injection'], `next_tasks[${index}].--test-injection`),
@@ -331,7 +331,7 @@ export function validatePlannerProgress(job, result) {
331
331
  throw new Error('Planner returned in_progress with no executable tasks or unblocks while no live work remains; return satisfied when only Relay-owned review/merge/completion effects remain');
332
332
  }
333
333
  export function validatePlannerLifecycleOwnership(job, specs) {
334
- if (job.prNumber !== 0 || !job.localMerge)
334
+ if (!job.localMerge?.sourceRef)
335
335
  return;
336
336
  for (const spec of specs) {
337
337
  // Restrict ownership detection to an individual imperative clause. A long
@@ -347,6 +347,11 @@ export function validatePlannerLifecycleOwnership(job, specs) {
347
347
  if (ownsLifecycle && [localMerge, jobCompletion, externalMerge].filter(Boolean).length >= 2) {
348
348
  throw new Error(`Planner next task ${spec.id} attempts to own Graph lifecycle delivery; local merge, Job COMPLETED, and projected PR merge are Relay-owned effects and must not be worker Tasks`);
349
349
  }
350
+ const releaseEffect = /\b(?:publish|release|deploy)\b.{0,100}\b(?:package|npm|service|runtime|version)\b|\b(?:package|npm|service|runtime)\b.{0,100}\b(?:publish|release|deploy)\b/.test(clause);
351
+ const releaseImperative = /^\s*(?:publish|release|deploy)\b|\b(?:then|and)\s+(?:publish|release|deploy)\b/.test(clause);
352
+ if (releaseEffect && releaseImperative) {
353
+ throw new Error(`Planner next task ${spec.id} attempts to own Relay release delivery; package publication and service/runtime deployment are Relay-owned effects and must not be worker Tasks`);
354
+ }
350
355
  }
351
356
  }
352
357
  }
@@ -431,6 +436,7 @@ export function plannerInput(context) {
431
436
  visible_agents: visibleAgents ?? [],
432
437
  tasks: context.tasks.map(task => ({
433
438
  id: task.id,
439
+ title: task.title,
434
440
  kind: task.kind ?? 'work',
435
441
  parent_task_id: task.parentTaskId,
436
442
  state: task.state,
package/dist/pool.js CHANGED
@@ -12,7 +12,9 @@ export function needsLifecycleReconciliation(job, now = Date.now()) {
12
12
  // DRAFT create-only Jobs stay dormant until a caller explicitly creates work.
13
13
  if (job.tasks.length === 0)
14
14
  return false;
15
- if (job.tasks.some(task => (task.state === 'QUEUED' && task.executionId === null) || (task.state === 'DRAFT' && task.dependencies.length > 0)))
15
+ if (job.tasks.some(task => (task.state === 'QUEUED' && task.executionId === null)
16
+ || (task.state === 'RUNNING' && task.adapter === 'orchestrator' && task.executionId === null)
17
+ || (task.state === 'DRAFT' && task.dependencies.length > 0)))
16
18
  return true;
17
19
  return job.tasks.some(task => ['RUNNING', 'QUEUED'].includes(task.state) && task.executionId !== null && task.leaseExpiresAt !== null && new Date(task.leaseExpiresAt).getTime() <= now);
18
20
  }
@@ -122,7 +122,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
122
122
  throw new Error('Too many final review rounds for ' + headSha);
123
123
  }
124
124
  externalReviewPrNumber(job) {
125
- return job.externalPrNumber && job.externalPrNumber > 0 ? job.externalPrNumber : job.prNumber;
125
+ return job.prNumber;
126
126
  }
127
127
  async createFinalReviewTask(job, headSha) {
128
128
  const id = this.nextFinalReviewTaskId(job, headSha);
@@ -163,7 +163,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
163
163
  // Local-first Jobs may expose a review-only GitHub PR. Once such a PR is
164
164
  // bound, its projected head must match the authoritative local head before
165
165
  // a reviewer is asked to review that immutable PR identity.
166
- if (!job.externalPrNumber || job.externalPrNumber <= 0 || !this.store.pullRequestHead)
166
+ if (!job.localMerge?.sourceRef || job.prNumber <= 0 || !this.store.pullRequestHead)
167
167
  return true;
168
168
  try {
169
169
  const projected = await this.store.pullRequestHead();
@@ -28,7 +28,7 @@ export { managedWorkerInput } from './managed-worker-input.js';
28
28
  import { recoverExpiredExecutions, recoverStartupOrphans as recoverStartupExecutionOrphans } from './execution-recovery.js';
29
29
  import { dispatchJobReconcile } from './workflow/job-reconcile-v2.js';
30
30
  import { reconcileLocalGitCompletion } from './local-merge-delivery.js';
31
- import { applyOrchestratorRescue } from './workflow/orchestrator-rescue.js';
31
+ import { repairLegacyOrchestrators } from './legacy-orchestrator-repair.js';
32
32
  import { launchPlanExecution } from './planner-runtime.js';
33
33
  import { classifyFailure, errorMessage, observability, withObservabilityCorrelation } from './observability.js';
34
34
  const PRIORITY_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
@@ -93,13 +93,7 @@ export class Reconciler extends ReconcilerReviewBase {
93
93
  if (job.state !== 'ACTIVE')
94
94
  return job;
95
95
  await recoverExpiredExecutions(job, now, this.executionRecoveryPorts(job));
96
- for (const task of (await this.store.load(jobId)).tasks) {
97
- const rescue = applyOrchestratorRescue(task, now);
98
- if (!rescue.rescued)
99
- continue;
100
- await this.store.saveTask(task);
101
- await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'task.orchestrator_rescued', 'queued', `Stale orchestrator task ${task.id} converted to ChatGPT execution`, 'orchestrator', { ageMs: rescue.ageMs, adapter: 'chatgpt' }, { agent: 'relay', component: 'agents-relay' }));
102
- }
96
+ await repairLegacyOrchestrators(jobId, now, { store: this.store, owner: this.options.owner, emit: event => this.emit(event) });
103
97
  await this.recoverPlannerChildren(job);
104
98
  await retryPendingPlannerUnblocks({
105
99
  store: this.store,
@@ -4,6 +4,7 @@ import { mkdirSync } from 'node:fs';
4
4
  import { readdir } from 'node:fs/promises';
5
5
  import { recordPublicationInDatabase, loadPublicationFromDatabase, listPublicationEventsFromDatabase } from './publication-store.js';
6
6
  import { observability } from './observability.js';
7
+ import { migrateLegacyJobPrNumbers, normalizeLegacyJobPrNumber } from './job-pr-number-migration.js';
7
8
  import { ProjectionOutbox, SCHEMA_VERSION, logicalOperationKey, } from './projection-outbox.js';
8
9
  function newerRecord(local, remote) {
9
10
  if (!local)
@@ -99,6 +100,7 @@ export class SQLiteRelayStore {
99
100
  ${ProjectionOutbox.createTableSql()}
100
101
  ${ProjectionOutbox.createIndexSql()}
101
102
  `);
103
+ migrateLegacyJobPrNumbers(this.db);
102
104
  const current = this.db.prepare("SELECT value FROM relay_meta WHERE key='schema_version'").get();
103
105
  const version = current?.value ? Number(current.value) : 0;
104
106
  if (version > SCHEMA_VERSION)
@@ -119,11 +121,11 @@ export class SQLiteRelayStore {
119
121
  }
120
122
  async loadJob(jobId) {
121
123
  const row = this.db.prepare('SELECT json FROM job_snapshot WHERE job_id=?').get(jobId);
122
- return row?.json ? JSON.parse(row.json) : null;
124
+ return row?.json ? normalizeLegacyJobPrNumber(JSON.parse(row.json)) : null;
123
125
  }
124
126
  async listJobs() {
125
127
  const rows = this.db.prepare('SELECT json FROM job_snapshot ORDER BY job_id').all();
126
- return rows.map(row => JSON.parse(row.json));
128
+ return rows.map(row => normalizeLegacyJobPrNumber(JSON.parse(row.json)));
127
129
  }
128
130
  async saveJob(job) {
129
131
  this.writeJobSnapshot(job, new Date().toISOString());
@@ -135,7 +137,7 @@ export class SQLiteRelayStore {
135
137
  const row = this.db.prepare('SELECT json FROM job_snapshot WHERE job_id=?').get(jobId);
136
138
  if (!row?.json)
137
139
  throw new Error(`Local authoritative Job ${jobId} not found`);
138
- const current = JSON.parse(row.json);
140
+ const current = normalizeLegacyJobPrNumber(JSON.parse(row.json));
139
141
  const before = JSON.stringify(current);
140
142
  const next = mutation(current);
141
143
  if (next.id !== jobId)
@@ -388,7 +390,7 @@ export async function discoverLocalRelayStoreSnapshots(directory) {
388
390
  try {
389
391
  db = new DatabaseSync(path, { readOnly: true });
390
392
  const rows = db.prepare('SELECT json FROM job_snapshot ORDER BY job_id').all();
391
- const jobs = rows.map(row => JSON.parse(row.json));
393
+ const jobs = rows.map(row => normalizeLegacyJobPrNumber(JSON.parse(row.json)));
392
394
  const row = db.prepare('SELECT state,head_ref,head_sha,observed_at FROM pull_request_facts WHERE id=1').get();
393
395
  const facts = row ? {
394
396
  state: row.state,
@@ -1,4 +1,5 @@
1
1
  import { normalizeThreadId } from './reviewer-thread.js';
2
+ import { supersedeCancelledTask } from './task-actions.js';
2
3
  export function createWorkspaceControl(deps) {
3
4
  const { client, trusted, noGitHub, repositories, refreshAll, requireLocalJobBinding, makeReconciler, concurrency, pool, bus, isEffectivelyBlocked, unblockAutonomousTasks, unblockTask, publishTaskUnblocked, dispatchTaskAction, registerBlockerDependency, discoverAgents, ensureManagedGitHubJob } = deps;
4
5
  const workspaceControl = async (body) => {
@@ -106,6 +107,15 @@ export function createWorkspaceControl(deps) {
106
107
  await pool.reconcileLocalJob(jobId, 'control.job.unblock');
107
108
  return await store.load(jobId);
108
109
  }
110
+ if (command === 'task.supersede') {
111
+ const taskId = typeof body.taskId === 'string' ? body.taskId : '';
112
+ const reason = typeof body.reason === 'string' ? body.reason : '';
113
+ if (!taskId || !reason.trim())
114
+ throw new Error('task supersede requires taskId and reason');
115
+ await supersedeCancelledTask(store, jobId, taskId, reason);
116
+ await pool.reconcileLocalJob(jobId, 'control.task.supersede');
117
+ return await store.load(jobId);
118
+ }
109
119
  if (command === 'task.recover') {
110
120
  const taskId = typeof body.taskId === 'string' ? body.taskId : '';
111
121
  const recoveryTaskId = typeof body.recoveryTaskId === 'string' ? body.recoveryTaskId : '';
@@ -3,11 +3,11 @@ import { allocateEntityId, isGeneratedEntityId } from './job-task-id-sequence.js
3
3
  import { relayStoreDirectory } from './relay-store.js';
4
4
  import { dispatchJobLifecycleSync } from './workflow/lifecycle.js';
5
5
  export function createDashboardActions(deps) {
6
- const { getOverviews, requireLocalJobBinding, pool, dashboardJobAction, makeReconciler, concurrency, dashboardJobSetState, refreshAll, applyJobActivateAction, applyOrchestratorStartAction, applyOrchestratorCompleteAction, publishOrchestratorTaskStarted, publishOrchestratorTaskCompletion, bus, dashboardTaskAction, dashboardTaskSetState, storeDirectory } = deps;
6
+ const { getOverviews, requireLocalJobBinding, pool, dashboardJobAction, makeReconciler, concurrency, dashboardJobSetState, refreshAll, applyJobActivateAction, bus, dashboardTaskAction, dashboardTaskSetState, storeDirectory } = deps;
7
7
  const currentOverviews = () => getOverviews();
8
8
  const dashboardOverview = (jobId, target) => currentOverviews().find((item) => item.job.id === jobId
9
9
  && (!target?.repository || item.job.repository === target.repository)
10
- && (!target?.prNumber || (item.job.externalPrNumber ?? item.job.prNumber) === target.prNumber));
10
+ && (!target?.prNumber || (item.job.prNumber) === target.prNumber));
11
11
  const workspaceJobAction = (action) => async (jobId) => {
12
12
  const overview = currentOverviews().find((item) => item.job.id === jobId);
13
13
  if (!overview)
@@ -84,36 +84,6 @@ export function createDashboardActions(deps) {
84
84
  await pool.reconcileLocalJob(jobId, 'control.task.create');
85
85
  return { ...(await store.load(jobId)), createdTaskId: task.id };
86
86
  };
87
- const workspaceTaskStart = async (jobId, taskId) => {
88
- const overview = currentOverviews().find((item) => item.job.id === jobId);
89
- if (!overview)
90
- throw new Error(`Managed job ${jobId} not found`);
91
- const store = (await requireLocalJobBinding(jobId)).store;
92
- const job = await store.load(jobId);
93
- const task = job.tasks.find((item) => item.id === taskId);
94
- if (!task)
95
- throw new Error(`Task ${taskId} not found`);
96
- await applyOrchestratorStartAction(task);
97
- await store.saveTask(task);
98
- await publishOrchestratorTaskStarted(bus, job, task);
99
- await pool.reconcileLocalJob(jobId, 'control.task.start');
100
- return await store.load(jobId);
101
- };
102
- const workspaceTaskComplete = async (jobId, taskId, summary) => {
103
- const overview = currentOverviews().find((item) => item.job.id === jobId);
104
- if (!overview)
105
- throw new Error(`Managed job ${jobId} not found`);
106
- const store = (await requireLocalJobBinding(jobId)).store;
107
- const job = await store.load(jobId);
108
- const task = job.tasks.find((item) => item.id === taskId);
109
- if (!task)
110
- throw new Error(`Task ${taskId} not found`);
111
- await applyOrchestratorCompleteAction(task, summary);
112
- await store.saveTask(task);
113
- await publishOrchestratorTaskCompletion(bus, job, task, summary);
114
- await pool.reconcileLocalJob(jobId, 'control.task.complete');
115
- return await store.load(jobId);
116
- };
117
87
  const workspaceTaskAction = (action) => async (jobId, taskId) => {
118
88
  const overview = currentOverviews().find((item) => item.job.id === jobId);
119
89
  if (!overview)
@@ -130,5 +100,5 @@ export function createDashboardActions(deps) {
130
100
  await refreshAll();
131
101
  return result;
132
102
  };
133
- return { dashboardOverview, workspaceJobAction, workspaceJobSetState, workspaceTaskCreate, workspaceTaskStart, workspaceTaskComplete, workspaceTaskAction, workspaceTaskSetState };
103
+ return { dashboardOverview, workspaceJobAction, workspaceJobSetState, workspaceTaskCreate, workspaceTaskAction, workspaceTaskSetState };
134
104
  }
package/dist/relayd.js CHANGED
@@ -24,14 +24,13 @@ import { githubAuthContext } from './github-auth.js';
24
24
  import { DisabledGitHubClient, noGitHubMode } from './no-github.js';
25
25
  import { basename } from 'node:path';
26
26
  import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
27
- import { eventBusResolutionMessage, publishOrchestratorTaskStarted, publishOrchestratorTaskCompletion, publishTaskUnblocked, resolveEventBus } from './cli-support.js';
27
+ import { eventBusResolutionMessage, publishTaskUnblocked, resolveEventBus } from './cli-support.js';
28
28
  import { eventFor } from './events.js';
29
29
  import { dispatchTaskAction } from './task-actions.js';
30
30
  import { isEffectivelyBlocked, unblockAutonomousTasks, unblockTask } from './unblock.js';
31
31
  import { registerBlockerDependency } from './blockers.js';
32
32
  import { ensureManagedGitHubJob } from './cli-job-create.js';
33
33
  import { applyJobActivateAction } from './job-actions.js';
34
- import { applyOrchestratorStartAction, applyOrchestratorCompleteAction } from './task-actions.js';
35
34
  import { managedTaskUpdateHandler } from './task-update.js';
36
35
  import { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
37
36
  import { codexAndZaiUsageRegistry } from './usage.js';
@@ -155,7 +154,7 @@ export async function runDaemon(argv) {
155
154
  cachedOverviews = await inventoryFromLocal();
156
155
  const makeReconciler = (store, maxConcurrent) => new Reconciler(store, { noGitHub, owner: `relayd-${process.pid}`, maxConcurrent, leaseMs: Number(value(argv, '--lease-ms', '300000')), adapters: [new CodexAdapter(value(argv, '--codex', 'codex')), new ChatGptAdapter()], continuations: [new CodexThreadContinuation(value(argv, '--codex', 'codex')), new CommandContinuation(), new WebhookContinuation()], planner: runtimePlanner(argv), usageSnapshotProvider: () => usageRegistry.snapshot(), runtimeSummaryProvider: () => ({ chatgptRunning: summarizeRuntime(cachedOverviews.map(item => item.job)).workers.chatgpt }), decisionEngine: executionDecisionEngine(), decisionMode: decisionMode(), decisionAuditStore, eventBus: bus, reviewer: runtimeReviewer(argv), projectPathForJob: async (job) => {
157
156
  const canonical = repositoryPaths.get(job.repository) ?? await resolveManagedProjectPath(job.repository, explicitProjectPath, workspaceRoot);
158
- if (job.prNumber !== 0 || job.schedule)
157
+ if (!job.localMerge?.sourceRef || job.schedule)
159
158
  return resolveRepositoryProjectPath(canonical, job.projectPath);
160
159
  const sourceRef = await ensureLocalJobSourceBranch(canonical, job);
161
160
  if (!store.mutateJob)
@@ -243,15 +242,15 @@ export async function runDaemon(argv) {
243
242
  // A completed local Job may never have had a display PR. Publish main
244
243
  // regardless: GitHub availability cannot determine SQLite completion.
245
244
  if (discovered.state === 'COMPLETED' && discovered.localMerge?.status === 'succeeded'
246
- && (discovered.externalPrNumber ?? discovered.prNumber) <= 0) {
245
+ && discovered.prNumber <= 0) {
247
246
  const published = projectAuthoritativeLocalMerge(canonical, discovered);
248
247
  if (published.status === 'projected')
249
248
  await bus?.publish(eventFor(discovered.id, 'projection', null, 'projection.delivered', 'succeeded', `GitHub main synchronization ${published.status}`, 'orchestrator', { operation: 'github.syncMain', deliveryStatus: published.status, mergedCommitSha: published.localMergedCommitSha }, { agent: 'relay', component: 'github-projection' }));
250
249
  continue;
251
250
  }
252
- const existingExternalPr = discovered.externalPrNumber ?? (discovered.prNumber > 0 ? discovered.prNumber : 0);
251
+ const existingPrNumber = discovered.prNumber > 0 ? discovered.prNumber : 0;
253
252
  const terminal = ['COMPLETED', 'CANCELLED'].includes(discovered.state);
254
- if (terminal && existingExternalPr <= 0)
253
+ if (terminal && existingPrNumber <= 0)
255
254
  continue;
256
255
  const sourceSha = await gitText(canonical, ['rev-parse', '--verify', sourceRef]).catch(() => '');
257
256
  if (!sourceSha)
@@ -275,10 +274,10 @@ export async function runDaemon(argv) {
275
274
  }));
276
275
  }
277
276
  let job = (await relay.loadJob(discovered.id)) ?? discovered;
278
- let externalPrNumber = job.externalPrNumber ?? (job.prNumber > 0 ? job.prNumber : 0);
279
- if (externalPrNumber === 0 && ['COMPLETED', 'CANCELLED'].includes(job.state))
277
+ let prNumber = job.prNumber > 0 ? job.prNumber : 0;
278
+ if (prNumber === 0 && ['COMPLETED', 'CANCELLED'].includes(job.state))
280
279
  continue;
281
- if (externalPrNumber === 0) {
280
+ if (prNumber === 0) {
282
281
  // Do not bootstrap an empty remote-only PR commit: it would diverge from
283
282
  // the real local Job branch before its first Worker commit. Publish the
284
283
  // review-only PR when there is an actual diff against main.
@@ -295,17 +294,17 @@ export async function runDaemon(argv) {
295
294
  const pr = await resolvePullRequest(client, job.repository, { head: branch, base, title: job.title, body: job.objective ?? job.description ?? '' });
296
295
  if (!relay.mutateJob)
297
296
  throw new Error('Atomic SQLite mutation is required for GitHub PR binding');
298
- job = await relay.mutateJob(job.id, current => ({ ...current, externalPrNumber: pr.number, updatedAt: new Date().toISOString() }));
299
- externalPrNumber = pr.number;
297
+ job = await relay.mutateJob(job.id, current => ({ ...current, prNumber: pr.number, updatedAt: new Date().toISOString() }));
298
+ prNumber = pr.number;
300
299
  observability().record({ stage: 'projection', event: 'github.pr_bound', outcome: 'succeeded', message: `Bound local Job ${job.id} to GitHub PR #${pr.number}`, correlation: { repository: job.repository, prNumber: pr.number, jobId: job.id }, data: { head: branch, base } });
301
300
  }
302
- if (externalPrNumber <= 0)
301
+ if (prNumber <= 0)
303
302
  continue;
304
303
  const latestFacts = await relay.loadPullRequestFacts();
305
304
  if (latestFacts?.state !== 'MERGED' && (latestFacts?.head?.sha !== sourceSha || latestFacts?.head?.ref !== branch)) {
306
305
  await relay.savePullRequestFacts({ state: latestFacts?.state === 'CLOSED' ? 'CLOSED' : 'OPEN', head: { ref: branch, sha: sourceSha }, observedAt: new Date().toISOString() });
307
306
  }
308
- const github = new GitHubStore(client, job.repository, externalPrNumber, trusted);
307
+ const github = new GitHubStore(client, job.repository, prNumber, trusted);
309
308
  projectionWriters.set(`job:${job.id}`, new GitHubProjectionWriter(relay, github, undefined, true));
310
309
  if (job.state === 'COMPLETED' && job.localMerge?.status === 'succeeded' && job.localMerge.projectionStrategy === 'target_ref_push') {
311
310
  const injection = await consumeMethodInjection(relay, job.repository, job.id, 'github.projectLocalMerge');
@@ -314,14 +313,14 @@ export async function runDaemon(argv) {
314
313
  // A review-only PR is never merged by GitHub. Close it after local main
315
314
  // has been safely published, retaining the diff, comments and commit link.
316
315
  const remotePr = await github.pullRequestState();
317
- await archiveDisplayPullRequest(client, job.repository, externalPrNumber, job.id, projectedMerge.projectedCommitSha, remotePr);
316
+ await archiveDisplayPullRequest(client, job.repository, prNumber, job.id, projectedMerge.projectedCommitSha, remotePr);
318
317
  const projectedState = await github.pullRequestState();
319
318
  const projectedHead = await github.pullRequestHead();
320
319
  await relay.savePullRequestFacts({ state: projectedState, head: projectedHead, observedAt: new Date().toISOString() });
321
320
  observability().record({
322
321
  stage: 'projection', event: 'github.local_merge_projected', outcome: 'succeeded',
323
322
  message: `Projected authoritative local merge ${projectedMerge.localMergedCommitSha} to ${projectedMerge.targetRef}`,
324
- correlation: { repository: job.repository, prNumber: externalPrNumber, jobId: job.id },
323
+ correlation: { repository: job.repository, prNumber, jobId: job.id },
325
324
  data: { ...projectedMerge, githubState: projectedState },
326
325
  });
327
326
  }
@@ -334,7 +333,7 @@ export async function runDaemon(argv) {
334
333
  observability().record({
335
334
  stage: 'projection', event: 'github.binding_job_failed', level: 'warn', outcome: 'failed',
336
335
  message: `GitHub binding/projection failed for ${discovered.id}: ${reason}`,
337
- correlation: { repository: discovered.repository, prNumber: discovered.externalPrNumber ?? discovered.prNumber, jobId: discovered.id },
336
+ correlation: { repository: discovered.repository, prNumber: discovered.prNumber, jobId: discovered.id },
338
337
  data: { classification, reason },
339
338
  });
340
339
  }
@@ -362,7 +361,7 @@ export async function runDaemon(argv) {
362
361
  const initial = cachedOverviews;
363
362
  const fallback = initial[0]?.job;
364
363
  const dashboardStore = fallback ? (await requireLocalJobBinding(fallback.id)).store : new InMemoryStore();
365
- const { dashboardOverview, workspaceJobAction, workspaceJobSetState, workspaceTaskCreate, workspaceTaskStart, workspaceTaskComplete, workspaceTaskAction, workspaceTaskSetState } = createDashboardActions({ getOverviews: () => cachedOverviews, requireLocalJobBinding, pool, dashboardJobAction, makeReconciler, concurrency, dashboardJobSetState, refreshAll, applyJobActivateAction, applyOrchestratorStartAction, applyOrchestratorCompleteAction, publishOrchestratorTaskStarted, publishOrchestratorTaskCompletion, bus, dashboardTaskAction, dashboardTaskSetState });
364
+ const { dashboardOverview, workspaceJobAction, workspaceJobSetState, workspaceTaskCreate, workspaceTaskAction, workspaceTaskSetState } = createDashboardActions({ getOverviews: () => cachedOverviews, requireLocalJobBinding, pool, dashboardJobAction, makeReconciler, concurrency, dashboardJobSetState, refreshAll, applyJobActivateAction, bus, dashboardTaskAction, dashboardTaskSetState });
366
365
  const workspaceControl = createWorkspaceControl({ client, trusted, noGitHub, repositories, refreshAll, requireLocalJobBinding, makeReconciler, concurrency, pool, bus, isEffectivelyBlocked, unblockAutonomousTasks, unblockTask, publishTaskUnblocked, dispatchTaskAction, registerBlockerDependency, discoverAgents, ensureManagedGitHubJob });
367
366
  const secretFile = value(argv, '--github-webhook-secret-file');
368
367
  const secret = noGitHub ? '' : value(argv, '--github-webhook-secret') || process.env.AGENTS_RELAY_GITHUB_WEBHOOK_SECRET || (secretFile ? (await readFile(secretFile, 'utf8')).trim() : '');
@@ -370,7 +369,7 @@ export async function runDaemon(argv) {
370
369
  secret,
371
370
  repositories: new Set(repositories),
372
371
  wake: async (event) => {
373
- const targets = cachedOverviews.filter(item => item.job.repository === event.repository && (event.jobId ? item.job.id === event.jobId : (item.job.externalPrNumber ?? item.job.prNumber) === event.pullRequest));
372
+ const targets = cachedOverviews.filter(item => item.job.repository === event.repository && (event.jobId ? item.job.id === event.jobId : item.job.prNumber === event.pullRequest));
374
373
  if (targets.length === 0)
375
374
  return;
376
375
  for (const target of targets) {
@@ -429,10 +428,8 @@ export async function runDaemon(argv) {
429
428
  const binding = await requireLocalJobBinding(jobId);
430
429
  return { store: binding.store, repository: binding.job.repository, prNumber: binding.job.prNumber };
431
430
  }, async (_repository, _prNumber, jobId) => pool.reconcileLocalJob(jobId, 'control.task.update')),
432
- taskStart: workspaceTaskStart,
433
431
  taskRetry: workspaceTaskAction('retry'),
434
432
  taskCancel: workspaceTaskAction('cancel'),
435
- taskComplete: workspaceTaskComplete,
436
433
  taskSetState: workspaceTaskSetState,
437
434
  }, async ({ jobId, taskId }) => {
438
435
  const overview = cachedOverviews.find(item => item.job.id === jobId);
@@ -42,7 +42,7 @@ export function prepareReviewerThreadDelivery(job, task, override) {
42
42
  return {
43
43
  kind: 'new-turn', threadId: destination.threadId, threadSource: destination.source,
44
44
  requestId: existing?.requestId ?? `${job.id}:${task.id}:${task.reviewHeadSha ?? job.headSha ?? 'review'}`,
45
- jobId: job.id, prNumber: job.externalPrNumber && job.externalPrNumber > 0 ? job.externalPrNumber : job.prNumber,
45
+ jobId: job.id, prNumber: job.prNumber,
46
46
  ...(task.reviewHeadSha ? { reviewHeadSha: task.reviewHeadSha } : {}),
47
47
  projectId: task.routing?.projectId ?? null,
48
48
  ...(existing?.executionId ? { executionId: existing.executionId } : {}),
@@ -1,5 +1,6 @@
1
1
  function isSuperseded(task) {
2
- return task.state === 'CANCELLED' && typeof task.error === 'string' && task.error.startsWith('Superseded ');
2
+ return task.state === 'CANCELLED' && ((task.resolution?.path === 'cancel_supersede' && task.resolution.state === 'resolved')
3
+ || (typeof task.error === 'string' && task.error.startsWith('Superseded ')));
3
4
  }
4
5
  export function implementationTasks(job) {
5
6
  return job.tasks.filter(task => task.kind !== 'planner'
@@ -7,8 +7,6 @@ export const TASK_UNBLOCK_RETRY_ACTION = 'task.unblock.retry';
7
7
  export const TASK_CANCEL_ACTION = 'task.cancel';
8
8
  export const TASK_RELEASE_ACTION = 'task.release';
9
9
  export const TASK_HOLD_ACTION = 'task.hold';
10
- export const TASK_START_ACTION = 'task.start';
11
- export const TASK_COMPLETE_ACTION = 'task.complete';
12
10
  export const TASK_QUEUE_ACTION = 'task.queue';
13
11
  function lifecycleRejection(task, action) {
14
12
  return new Error(`Task ${task.id} is not ${action} (${task.state})`);
@@ -100,42 +98,6 @@ export async function applyTaskQueueAction(job, task) {
100
98
  clearExecution(task);
101
99
  return task;
102
100
  }
103
- export async function applyOrchestratorStartAction(task) {
104
- if (task.adapter !== 'orchestrator')
105
- throw new Error('task start is only valid for adapter=orchestrator self-tasks');
106
- const originalState = task.state;
107
- try {
108
- await transitionTaskEvent(task, 'task.started');
109
- }
110
- catch (error) {
111
- if (error instanceof Error && error.message.startsWith('No enabled workflow transition')) {
112
- throw new Error(`Task ${task.id} is ${originalState}; only QUEUED orchestrator tasks can be started`);
113
- }
114
- throw error;
115
- }
116
- task.attempt = Math.max(task.attempt, 1);
117
- task.updatedAt = new Date().toISOString();
118
- return task;
119
- }
120
- export async function applyOrchestratorCompleteAction(task, summary) {
121
- if (task.adapter !== 'orchestrator')
122
- throw new Error('task complete is only valid for adapter=orchestrator self-tasks');
123
- const originalState = task.state;
124
- if (task.state !== 'RUNNING')
125
- throw new Error(`Task ${task.id} is ${originalState}; orchestrator task must be started before completion`);
126
- task.result = { summary };
127
- clearExecution(task);
128
- try {
129
- await transitionTaskEvent(task, 'task.completed');
130
- }
131
- catch (error) {
132
- if (error instanceof Error && error.message.startsWith('No enabled workflow transition')) {
133
- throw new Error(`Task ${task.id} is ${originalState}; only RUNNING orchestrator tasks can be completed`);
134
- }
135
- throw error;
136
- }
137
- return task;
138
- }
139
101
  async function applySelectedRelatedTaskMutations(job, mutations) {
140
102
  const changed = [];
141
103
  for (const mutation of mutations) {
@@ -148,6 +110,45 @@ async function applySelectedRelatedTaskMutations(job, mutations) {
148
110
  }
149
111
  return changed;
150
112
  }
113
+ function markCancelledTaskSuperseded(task, reason) {
114
+ if (task.kind === 'planner')
115
+ throw new Error(`Legacy planner Task ${task.id} is read-only`);
116
+ if (task.state !== 'CANCELLED')
117
+ throw new Error(`Task ${task.id} must already be CANCELLED before supersede annotation`);
118
+ if (!reason.trim())
119
+ throw new Error('task supersede requires a reason');
120
+ const proposal = { path: 'cancel_supersede', targetTaskId: task.id, rootCause: reason.trim(), solution: 'Superseded by later corrected durable work' };
121
+ const decision = dispatchTaskResolutionSync(task, proposal, 'external', { cancelSupersede: () => {
122
+ task.resolution = { ...materializeResolution(task, proposal, 'external'), state: 'resolved' };
123
+ task.error = `Superseded ${reason.trim()}`;
124
+ task.updatedAt = new Date().toISOString();
125
+ } });
126
+ if (!decision.accepted)
127
+ throw new Error(`Task resolution graph rejected cancellation supersede for ${task.id}`);
128
+ }
129
+ export async function supersedeCancelledTask(store, jobId, taskId, reason) {
130
+ if (store.mutateJob) {
131
+ let updatedTask;
132
+ const updated = await store.mutateJob(jobId, current => {
133
+ const task = current.tasks.find(item => item.id === taskId);
134
+ if (!task)
135
+ throw new Error(`Task ${taskId} not found`);
136
+ markCancelledTaskSuperseded(task, reason);
137
+ updatedTask = structuredClone(task);
138
+ return current;
139
+ });
140
+ if (!updatedTask)
141
+ throw new Error(`Task ${taskId} was not updated`);
142
+ return { job: updated, task: updatedTask };
143
+ }
144
+ const job = await store.load(jobId);
145
+ const task = job.tasks.find(item => item.id === taskId);
146
+ if (!task)
147
+ throw new Error(`Task ${taskId} not found`);
148
+ markCancelledTaskSuperseded(task, reason);
149
+ await store.saveTask(task);
150
+ return { job: await store.load(jobId), task };
151
+ }
151
152
  /** Compatibility bridge: domain mutation only; adapters own wake/reconcile/output behavior. */
152
153
  export async function dispatchTaskAction(store, request) {
153
154
  if (store.mutateJob && [TASK_RETRY_ACTION, TASK_RECOVER_ACTION, TASK_CANCEL_ACTION].includes(request.id)) {
@@ -162,8 +163,11 @@ export async function dispatchTaskAction(store, request) {
162
163
  throw new Error(`task recover requires an active Troubleshooter recovery for ${task.id}`);
163
164
  }
164
165
  const relatedMutations = request.id === TASK_RECOVER_ACTION ? [] : selectRelatedTaskMutations(current, task, request.id);
165
- if (request.id === TASK_RETRY_ACTION)
166
+ if (request.id === TASK_RETRY_ACTION) {
166
167
  applyTaskRetrySnapshot(task, { extendBudget: true });
168
+ task.routing = undefined;
169
+ task.workerSelection = undefined;
170
+ }
167
171
  else if (request.id === TASK_RECOVER_ACTION) {
168
172
  const proposal = { path: 'exact_original_retry', targetTaskId: task.id, rootCause: task.failureReason ?? task.error ?? 'durable failure', solution: `Recover exact original Task via ${request.recoveryTaskId}` };
169
173
  const decision = dispatchTaskResolutionSync(task, proposal, 'troubleshooter', { activateRecovery: () => { applyTaskRetrySnapshot(task, { extendBudget: true }); task.resolution = { ...materializeResolution(task, proposal, 'troubleshooter'), state: 'resolved' }; } });
@@ -190,8 +194,11 @@ export async function dispatchTaskAction(store, request) {
190
194
  if (!task)
191
195
  throw new Error(`Task ${request.taskId} not found`);
192
196
  const relatedMutations = request.id === TASK_RECOVER_ACTION ? [] : selectRelatedTaskMutations(job, task, request.id);
193
- if (request.id === TASK_RETRY_ACTION)
197
+ if (request.id === TASK_RETRY_ACTION) {
194
198
  await applyTaskRetryAction(task, { extendBudget: true });
199
+ task.routing = undefined;
200
+ task.workerSelection = undefined;
201
+ }
195
202
  else if (request.id === TASK_RECOVER_ACTION) {
196
203
  const recovery = request.recoveryTaskId ? job.tasks.find(item => item.id === request.recoveryTaskId) : undefined;
197
204
  if (!recovery || recovery.agentName !== 'troubleshooter' || recovery.parentTaskId !== task.id || !['QUEUED', 'RUNNING'].includes(recovery.state))
@@ -209,8 +216,6 @@ export async function dispatchTaskAction(store, request) {
209
216
  await applyTaskReleaseAction(task);
210
217
  else if (request.id === TASK_HOLD_ACTION)
211
218
  await applyTaskHoldAction(task);
212
- else if (request.id === TASK_START_ACTION)
213
- await applyOrchestratorStartAction(task);
214
219
  else if (request.id === TASK_QUEUE_ACTION)
215
220
  await applyTaskQueueAction(job, task);
216
221
  else
@@ -0,0 +1,5 @@
1
+ export function deriveTaskTitle(input, fallback = 'Task') {
2
+ const first = String(input ?? '').split(/\r?\n/).map(line => line.trim()).find(Boolean) ?? fallback;
3
+ const cleaned = first.replace(/^#{1,6}\s+/, '').replace(/^[-*+]\s+/, '').replace(/^\d+[.)]\s+/, '').replace(/\s+/g, ' ').trim() || fallback;
4
+ return cleaned.length <= 96 ? cleaned : cleaned.slice(0, 93).trimEnd() + '...';
5
+ }
@@ -51,10 +51,10 @@ export function pendingTroubleshooterTasks(job) {
51
51
  const id = troubleshooterTaskId(task);
52
52
  let create = false;
53
53
  const decision = dispatchTroubleshooter({
54
- failedOrBlocked: ['FAILED', 'BLOCKED'].includes(task.state),
54
+ failedWork: task.state === 'FAILED',
55
55
  ordinaryWork: task.kind !== 'planner' && task.agentName !== 'troubleshooter',
56
56
  currentRun: belongsToCurrentRun(job, task),
57
- needsRecovery: !(task.state === 'BLOCKED' && Boolean(task.unblock)),
57
+ needsRecovery: true,
58
58
  idUnused: !existing.has(id),
59
59
  noRecoveryBarrier: !hasRecoveryBarrier(job, task),
60
60
  }, () => { create = true; });
@@ -48,7 +48,7 @@ export function usageForProvider(snapshot, provider, now = Date.now()) {
48
48
  return {};
49
49
  }
50
50
  export function workerCandidates(adapters, registered, snapshot) {
51
- return adapters.filter(adapter => adapter.name !== 'orchestrator').flatMap(adapter => {
51
+ return adapters.flatMap(adapter => {
52
52
  const workers = registered.filter(worker => worker.runtime.adapter === adapter.name);
53
53
  return (workers.length > 0 ? workers : [undefined]).map(worker => {
54
54
  const provider = worker?.runtime.provider ?? (adapter.name === 'chatgpt' ? 'chatgpt' : undefined);