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.
- package/README.md +3 -3
- package/dist/cli-help.js +5 -8
- package/dist/cli-review.js +1 -1
- package/dist/cli-support.js +2 -22
- package/dist/cli-worker-task.js +7 -6
- package/dist/cli.js +22 -59
- package/dist/dashboard-actions.js +2 -1
- package/dist/dashboard-html.js +5 -5
- package/dist/dashboard.js +0 -4
- package/dist/github-binding-order.js +1 -1
- package/dist/job-pr-number-migration.js +24 -0
- package/dist/job-task-id-sequence.js +16 -9
- package/dist/legacy-orchestrator-repair.js +51 -0
- package/dist/local-job-registry.js +2 -3
- package/dist/local-merge-delivery.js +1 -1
- package/dist/managed-worker-input.js +3 -4
- package/dist/planner.js +14 -8
- package/dist/pool.js +3 -1
- package/dist/reconciler-review.js +2 -2
- package/dist/reconciler.js +2 -8
- package/dist/relay-store.js +6 -4
- package/dist/relayd-control.js +10 -0
- package/dist/relayd-dashboard-actions.js +3 -33
- package/dist/relayd.js +17 -20
- package/dist/reviewer-thread.js +1 -1
- package/dist/runtime-status.js +2 -1
- package/dist/task-actions.js +47 -42
- package/dist/task-title.js +5 -0
- package/dist/troubleshooter.js +2 -2
- package/dist/worker-routing.js +1 -1
- package/dist/workflow/entity-creation.js +11 -1
- package/dist/workflow/orchestrator-rescue.js +62 -18
- package/dist/workflow/troubleshooter-v2.js +1 -1
- package/dist/workspace.js +5 -4
- package/package.json +1 -1
- package/skills/agents-relay/contracts/orchestrator-workflows.md +13 -12
- package/workflow/orchestrator-rescue.mmd +16 -1
- package/workflow/troubleshooter.mmd +4 -4
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import { dispatchJobLifecycleSync, dispatchTaskLifecycleSync } from './lifecycle.js';
|
|
2
|
+
import { deriveTaskTitle } from '../task-title.js';
|
|
2
3
|
function dependenciesSatisfied(job, dependencies) {
|
|
3
4
|
if (dependencies.length === 0)
|
|
4
5
|
return true;
|
|
5
6
|
const byId = new Map(job.tasks.map(task => [task.id, task]));
|
|
6
7
|
return dependencies.every(id => byId.get(id)?.state === 'SUCCEEDED');
|
|
7
8
|
}
|
|
9
|
+
function validateTaskRequirements(value) {
|
|
10
|
+
if (value.adapter === 'orchestrator') {
|
|
11
|
+
throw new Error('New Tasks cannot use the Relay-internal orchestrator adapter; declare worker requirements and let Execution Router select the route');
|
|
12
|
+
}
|
|
13
|
+
if (value.workerRequirements && !Array.isArray(value.workerRequirements.capabilities)) {
|
|
14
|
+
throw new Error(`Task ${value.id} workerRequirements.capabilities must be an array`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
8
17
|
/**
|
|
9
18
|
* Canonical creation boundary for new Task entities.
|
|
10
19
|
* Constructors provide metadata/facts only; the Task lifecycle graph derives
|
|
11
20
|
* whether a fresh Task remains DRAFT or becomes QUEUED.
|
|
12
21
|
*/
|
|
13
22
|
export function createTaskEntity(job, value, options = {}) {
|
|
14
|
-
|
|
23
|
+
validateTaskRequirements(value);
|
|
24
|
+
const task = { ...value, title: value.title?.trim() || deriveTaskTitle(value.input, value.agentName ? `${value.agentName} task` : 'Task'), state: 'DRAFT' };
|
|
15
25
|
if (!options.defer) {
|
|
16
26
|
dispatchTaskLifecycleSync(task, 'reconcile', {
|
|
17
27
|
dependenciesSatisfied: dependenciesSatisfied(job, task.dependencies),
|
|
@@ -1,42 +1,86 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { dispatchWorkflowGraphSync } from './dispatcher.js';
|
|
3
3
|
import { GraphRuntime } from './graph-v2.js';
|
|
4
|
+
import { tryTransitionTaskEvent } from '../scheduler.js';
|
|
5
|
+
import { taskOverallTimeoutMs } from '../types.js';
|
|
4
6
|
export const STALE_ORCHESTRATOR_TASK_MS = 30 * 60 * 1000;
|
|
5
7
|
const source = readFileSync(new URL('../../workflow/orchestrator-rescue.mmd', import.meta.url), 'utf8');
|
|
6
8
|
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
7
9
|
export const orchestratorRescueV2 = new GraphRuntime(source, {
|
|
8
|
-
states: new Set(['idle', 'check', 'skip', 'rescued']),
|
|
9
|
-
guards: {
|
|
10
|
-
|
|
10
|
+
states: new Set(['idle', 'check', 'external_completed', 'blocked', 'skip', 'rescued']),
|
|
11
|
+
guards: {
|
|
12
|
+
legacyActive: boolGuard('legacyActive'),
|
|
13
|
+
verifiedExternalCompletion: boolGuard('verifiedExternalCompletion'),
|
|
14
|
+
unknownStartedOutcome: boolGuard('unknownStartedOutcome'),
|
|
15
|
+
rescuable: boolGuard('rescuable'),
|
|
16
|
+
},
|
|
17
|
+
actions: {
|
|
18
|
+
'task.applyExternalCompletion': { id: 'task.applyExternalCompletion' },
|
|
19
|
+
'task.blockLegacyOrchestrator': { id: 'task.blockLegacyOrchestrator' },
|
|
20
|
+
'task.rescueOrchestrator': { id: 'task.rescueOrchestrator' },
|
|
21
|
+
},
|
|
11
22
|
events: { 'orchestrator.rescue.reconcile': { id: 'orchestrator.rescue.reconcile' } },
|
|
12
23
|
});
|
|
13
24
|
export function orchestratorRescueFacts(task, now) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
const legacyActive = task.kind !== 'planner' && task.adapter === 'orchestrator'
|
|
26
|
+
&& (task.state === 'QUEUED' || task.state === 'RUNNING');
|
|
27
|
+
const verifiedExternalCompletion = legacyActive && task.result !== null
|
|
28
|
+
&& task.result.data?.externalCompletionVerified === true;
|
|
29
|
+
return {
|
|
30
|
+
legacyActive,
|
|
31
|
+
verifiedExternalCompletion,
|
|
32
|
+
unknownStartedOutcome: legacyActive && task.state === 'RUNNING' && !verifiedExternalCompletion
|
|
33
|
+
&& (!task.leaseExpiresAt || Date.parse(task.leaseExpiresAt) <= now),
|
|
34
|
+
rescuable: legacyActive && task.state === 'QUEUED' && !verifiedExternalCompletion
|
|
35
|
+
&& (!task.leaseExpiresAt || Date.parse(task.leaseExpiresAt) <= now),
|
|
36
|
+
};
|
|
19
37
|
}
|
|
20
38
|
export function decideOrchestratorRescue(task, now) {
|
|
21
39
|
return dispatchWorkflowGraphSync(orchestratorRescueV2, {
|
|
22
40
|
initialState: 'idle', input: { transport: 'event', id: 'orchestrator.rescue.reconcile', payload: {} },
|
|
23
41
|
durableFacts: orchestratorRescueFacts(task, now), graphName: 'orchestrator-rescue',
|
|
24
|
-
isTransientState: state => state === 'check', maxSteps:
|
|
42
|
+
isTransientState: state => state === 'check', maxSteps: 4,
|
|
25
43
|
});
|
|
26
44
|
}
|
|
27
|
-
export function
|
|
45
|
+
export function prepareOrchestratorRepair(task, now) {
|
|
28
46
|
const decision = decideOrchestratorRescue(task, now);
|
|
29
|
-
const
|
|
47
|
+
const externallyCompleted = decision.accepted
|
|
48
|
+
&& decision.actions.some(action => action.id === 'task.applyExternalCompletion');
|
|
49
|
+
const blocked = decision.accepted
|
|
50
|
+
&& decision.actions.some(action => action.id === 'task.blockLegacyOrchestrator');
|
|
51
|
+
const rescued = externallyCompleted || blocked
|
|
52
|
+
|| (decision.accepted && decision.actions.some(action => action.id === 'task.rescueOrchestrator'));
|
|
30
53
|
const ageMs = now - Date.parse(task.createdAt);
|
|
54
|
+
if (decision.accepted && externallyCompleted) {
|
|
55
|
+
tryTransitionTaskEvent(task, 'task.completed');
|
|
56
|
+
task.leaseOwner = null;
|
|
57
|
+
task.leaseExpiresAt = null;
|
|
58
|
+
task.executionId = null;
|
|
59
|
+
task.threadId = null;
|
|
60
|
+
task.updatedAt = new Date(now).toISOString();
|
|
61
|
+
return { repaired: true, externallyCompleted: true, blocked: false, ageMs };
|
|
62
|
+
}
|
|
31
63
|
if (!rescued)
|
|
32
|
-
return {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
return { repaired: false, externallyCompleted: false, blocked: false, ageMs };
|
|
65
|
+
if (blocked) {
|
|
66
|
+
tryTransitionTaskEvent(task, 'task.blocked');
|
|
67
|
+
task.error = 'Legacy orchestrator execution outcome is unknown; automatic replay is unsafe. Verify external effects and create replacement validation/recovery work before unblocking.';
|
|
68
|
+
task.leaseOwner = null;
|
|
69
|
+
task.leaseExpiresAt = null;
|
|
70
|
+
task.executionId = null;
|
|
71
|
+
task.threadId = null;
|
|
72
|
+
task.updatedAt = new Date(now).toISOString();
|
|
73
|
+
return { repaired: true, externallyCompleted: false, blocked: true, ageMs };
|
|
74
|
+
}
|
|
75
|
+
task.adapter = 'codex';
|
|
76
|
+
task.workerRequirements = { capabilities: [...task.capabilities] };
|
|
37
77
|
task.workerSelection = undefined;
|
|
38
78
|
task.routing = undefined;
|
|
39
|
-
task.
|
|
79
|
+
task.leaseOwner = null;
|
|
80
|
+
task.leaseExpiresAt = null;
|
|
81
|
+
task.executionId = null;
|
|
82
|
+
task.threadId = null;
|
|
83
|
+
task.timeoutMs = taskOverallTimeoutMs(task.storySize ?? 'M');
|
|
40
84
|
task.updatedAt = new Date(now).toISOString();
|
|
41
|
-
return {
|
|
85
|
+
return { repaired: true, externallyCompleted: false, blocked: false, ageMs };
|
|
42
86
|
}
|
|
@@ -6,7 +6,7 @@ const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
|
6
6
|
export const troubleshooterV2 = new GraphRuntime(source, {
|
|
7
7
|
states: new Set(['idle', 'check_ordinary', 'check_run', 'check_needs', 'check_id', 'check_barrier', 'skip', 'create']),
|
|
8
8
|
guards: {
|
|
9
|
-
|
|
9
|
+
failedWork: boolGuard('failedWork'),
|
|
10
10
|
ordinaryWork: boolGuard('ordinaryWork'),
|
|
11
11
|
currentRun: boolGuard('currentRun'),
|
|
12
12
|
needsRecovery: boolGuard('needsRecovery'),
|
package/dist/workspace.js
CHANGED
|
@@ -217,7 +217,7 @@ export async function ensureLocalJobSourceBranch(projectPath, job) {
|
|
|
217
217
|
return sourceRef;
|
|
218
218
|
}
|
|
219
219
|
export async function prepareLocalJobExecutionWorktree(projectPath, job, workspaceRoot = defaultWorkspaceRoot()) {
|
|
220
|
-
if (
|
|
220
|
+
if (!job.localMerge?.sourceRef)
|
|
221
221
|
return resolveRepositoryProjectPath(projectPath, job.projectPath);
|
|
222
222
|
const sourceRef = job.localMerge.sourceRef;
|
|
223
223
|
if (!/^refs\/heads\/[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(sourceRef) || sourceRef.includes('..') || sourceRef.endsWith('.lock'))
|
|
@@ -285,14 +285,15 @@ export async function prepareLocalJobExecutionWorktree(projectPath, job, workspa
|
|
|
285
285
|
export async function cleanupCompletedJobWorktree(projectPath, job, workspaceRoot = defaultWorkspaceRoot()) {
|
|
286
286
|
if (job.state !== 'COMPLETED')
|
|
287
287
|
return { status: 'blocked', reason: 'Job is not completed' };
|
|
288
|
-
const
|
|
288
|
+
const isLocalJob = Boolean(job.localMerge?.sourceRef);
|
|
289
|
+
const expectedHead = isLocalJob ? (job.localMerge?.sourceSha ?? job.review?.headSha ?? job.headSha) : job.headSha;
|
|
289
290
|
if (!expectedHead)
|
|
290
291
|
return { status: 'blocked', reason: 'No durable Job head SHA' };
|
|
291
292
|
if (job.tasks.some(task => task.executionId !== null || task.leaseOwner !== null))
|
|
292
293
|
return { status: 'blocked', reason: 'Task still owns an execution or lease' };
|
|
293
294
|
if ((job.plans ?? []).some(plan => plan.executionId !== null || plan.leaseOwner !== null))
|
|
294
295
|
return { status: 'blocked', reason: 'Planner still owns an execution or lease' };
|
|
295
|
-
const worktree =
|
|
296
|
+
const worktree = isLocalJob ? managedLocalJobWorktreePath(job.repository, job.id, workspaceRoot) : managedJobWorktreePath(job.repository, job.prNumber, workspaceRoot);
|
|
296
297
|
let metadata;
|
|
297
298
|
try {
|
|
298
299
|
metadata = await lstat(worktree);
|
|
@@ -311,7 +312,7 @@ export async function cleanupCompletedJobWorktree(projectPath, job, workspaceRoo
|
|
|
311
312
|
}
|
|
312
313
|
catch { /* Local-only repositories may have no origin. */ }
|
|
313
314
|
const originRepository = origin ? githubRepositoryFromRemote(origin) : null;
|
|
314
|
-
if ((job.prNumber > 0 && originRepository !== job.repository) || (
|
|
315
|
+
if ((job.prNumber > 0 && !isLocalJob && originRepository !== job.repository) || (isLocalJob && originRepository && originRepository !== job.repository))
|
|
315
316
|
return { status: 'blocked', reason: 'Canonical repository identity mismatch' };
|
|
316
317
|
const registered = await git(primary, ['worktree', 'list', '--porcelain']);
|
|
317
318
|
const ownedRoot = await realpath(worktree);
|
package/package.json
CHANGED
|
@@ -24,18 +24,19 @@ The initial decomposition belongs to that durable Planner round. The orchestrato
|
|
|
24
24
|
|
|
25
25
|
## P0 / self job workflow
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
`job create`
|
|
27
|
+
P0/self work is ordinary durable Relay work. The orchestrator may choose normal
|
|
28
|
+
Task requirements, but every newly created work Task is executable by a
|
|
29
|
+
worker/model route selected by Execution Router. Relay has no privileged
|
|
30
|
+
orchestrator-worker lifecycle, and new Task creation rejects
|
|
31
|
+
`adapter: orchestrator`. P0/self jobs do not require the normal reviewer gate
|
|
32
|
+
unless the user explicitly asks for review; Relay-owned merge and completion
|
|
33
|
+
lifecycle remain in their canonical graphs.
|
|
34
|
+
|
|
35
|
+
Legacy recovery: existing `adapter: orchestrator` work Tasks may be repaired only
|
|
36
|
+
through the canonical rescue graph. The repair converts only never-started QUEUED records in place to declarative worker requirements and lets Execution Router select the route. A legacy RUNNING record with an unknown outcome is quarantined as BLOCKED instead of replayed; only explicit durable external-completion verification may complete it without execution. Process reclamation is attempted only after the record is proven eligible for this legacy path, so normal live workers are never touched by the migration scan.
|
|
37
|
+
|
|
38
|
+
`job create --initial-plan` remains the normal way to create durable planning
|
|
39
|
+
state and hand subsequent execution to Relay.
|
|
39
40
|
|
|
40
41
|
## Merge completion vs deployment evidence
|
|
41
42
|
|
|
@@ -2,12 +2,27 @@ flowchart LR
|
|
|
2
2
|
%% relay: initial idle
|
|
3
3
|
idle["state: idle"]
|
|
4
4
|
check["state: check"]
|
|
5
|
+
external["state: external_completed"]
|
|
6
|
+
blocked["state: blocked"]
|
|
5
7
|
skip["state: skip"]
|
|
6
8
|
rescued["state: rescued"]
|
|
9
|
+
legacyActive["guard: legacyActive"]
|
|
10
|
+
verifiedExternalCompletion["guard: verifiedExternalCompletion"]
|
|
11
|
+
unknownStartedOutcome["guard: unknownStartedOutcome"]
|
|
7
12
|
rescuable["guard: rescuable"]
|
|
8
13
|
rescue["action: task.rescueOrchestrator"]
|
|
14
|
+
externalCompletion["action: task.applyExternalCompletion"]
|
|
15
|
+
quarantine["action: task.blockLegacyOrchestrator"]
|
|
9
16
|
idle -->|on: orchestrator.rescue.reconcile| check
|
|
10
|
-
check -->|on: orchestrator.rescue.reconcile|
|
|
17
|
+
check -->|on: orchestrator.rescue.reconcile| legacyActive
|
|
18
|
+
legacyActive -->|false| skip
|
|
19
|
+
legacyActive -->|true| verifiedExternalCompletion
|
|
20
|
+
verifiedExternalCompletion -->|true| externalCompletion
|
|
21
|
+
externalCompletion -->|to: external_completed| external
|
|
22
|
+
verifiedExternalCompletion -->|false| unknownStartedOutcome
|
|
23
|
+
unknownStartedOutcome -->|true| quarantine
|
|
24
|
+
quarantine -->|to: blocked| blocked
|
|
25
|
+
unknownStartedOutcome -->|false| rescuable
|
|
11
26
|
rescuable -->|false| skip
|
|
12
27
|
rescuable -->|true| rescue
|
|
13
28
|
rescue -->|to: rescued| rescued
|
|
@@ -9,7 +9,7 @@ checkBarrier["state: check_barrier"]
|
|
|
9
9
|
skip["state: skip"]
|
|
10
10
|
create["state: create"]
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
failed["guard: failedWork"]
|
|
13
13
|
ordinary["guard: ordinaryWork"]
|
|
14
14
|
currentRun["guard: currentRun"]
|
|
15
15
|
needsRecovery["guard: needsRecovery"]
|
|
@@ -17,9 +17,9 @@ idUnused["guard: idUnused"]
|
|
|
17
17
|
noBarrier["guard: noRecoveryBarrier"]
|
|
18
18
|
createAction["action: troubleshooter.create"]
|
|
19
19
|
|
|
20
|
-
idle -->|on: troubleshooter.reconcile|
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
idle -->|on: troubleshooter.reconcile| failed
|
|
21
|
+
failed -->|false| skip
|
|
22
|
+
failed -->|true| checkOrdinary
|
|
23
23
|
|
|
24
24
|
checkOrdinary -->|on: troubleshooter.reconcile| ordinary
|
|
25
25
|
ordinary -->|false| skip
|