agents-relay 1.0.54 → 1.0.55
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 +6 -1
- package/dist/cli.js +26 -10
- package/dist/dashboard.js +2 -2
- package/dist/pool.js +2 -2
- package/dist/reconciler.js +4 -4
- package/dist/scheduled-jobs.js +1 -1
- package/dist/scheduler.js +7 -2
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/skills/agents-relay/contracts/task-job-contract.md +12 -0
package/README.md
CHANGED
|
@@ -23,12 +23,17 @@ npx agents-relay reconcile --repo OWNER/REPO --pr 12 --id job-1
|
|
|
23
23
|
npx agents-relay job show --repo OWNER/REPO --pr 12 --id job-1
|
|
24
24
|
~~~
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Task readiness is explicit: `DRAFT` means the durable task exists but is not executable yet; `QUEUED` means Relay may execute it now; `RUNNING` means worker execution has started. Tasks created with dependencies start `DRAFT` and become `QUEUED` when those dependencies succeed. A dependency-free task created with `--draft` stays held until `task update --queue`. Once a task has run, external waiting is represented as `BLOCKED`, never by returning to `DRAFT`.
|
|
27
|
+
|
|
28
|
+
Mutable draft/queued/retryable task fields can be changed in place without replacing the durable task identity. `task update` rejects `RUNNING`, `SUCCEEDED`, and `CANCELLED` tasks so active execution and terminal history cannot be rewritten. For example:
|
|
27
29
|
|
|
28
30
|
~~~sh
|
|
29
31
|
npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id child \
|
|
30
32
|
--adapter chatgpt --provider chatgpt --model gpt-5.6-sol --reasoning high \
|
|
31
33
|
--capabilities model,chatgpt,browser-harness
|
|
34
|
+
|
|
35
|
+
# Explicitly release a held DRAFT task
|
|
36
|
+
npx agents-relay task update --repo OWNER/REPO --pr 12 --id job-1 --task-id child --queue
|
|
32
37
|
~~~
|
|
33
38
|
|
|
34
39
|
Jobs default to `fixed`. `job create` persists only the durable job and never
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import { serveDashboard } from './dashboard.js';
|
|
|
15
15
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
16
16
|
import { discoverAgents } from './registry.js';
|
|
17
17
|
import { AgentObjectivePlanner, CommandObjectivePlanner } from './planner.js';
|
|
18
|
-
import { extendRetryBudget, parseJobSchedule } from './scheduler.js';
|
|
18
|
+
import { extendRetryBudget, parseJobSchedule, transitionTask } from './scheduler.js';
|
|
19
19
|
import { dueBuckets, processDueScheduledJobs, syncScheduleLabels } from './scheduled-jobs.js';
|
|
20
20
|
import { resolveExecutionContext } from './execution-context.js';
|
|
21
21
|
import { defaultWorkspaceRoot, resolveManagedProjectPath } from './workspace.js';
|
|
@@ -93,16 +93,16 @@ Usage:
|
|
|
93
93
|
npx agents-relay task <create|update|show|list|retry|cancel> [options]
|
|
94
94
|
|
|
95
95
|
Actions:
|
|
96
|
-
create Create one
|
|
96
|
+
create Create one durable Task under an existing Job
|
|
97
97
|
update Update mutable Task fields
|
|
98
98
|
show Show one Task
|
|
99
99
|
list List Tasks for a Job
|
|
100
100
|
retry Retry a failed or blocked Task
|
|
101
101
|
cancel Cancel a Task`,
|
|
102
|
-
'task create': `Create one
|
|
102
|
+
'task create': `Create one durable Task under an existing Job.
|
|
103
103
|
|
|
104
104
|
Required: normal Job storage options plus --input TEXT and --output task-pr|file. Codex/ChatGPT tasks also require --model MODEL.
|
|
105
|
-
Common: --task-id ID, --adapter codex|chatgpt|orchestrator, --priority P0|P1|P2|P3, --story-size S|M|L, --deps a,b, --parent ID.
|
|
105
|
+
Common: --task-id ID, --adapter codex|chatgpt|orchestrator, --priority P0|P1|P2|P3, --story-size S|M|L, --deps a,b, --parent ID, --draft.\nTasks with dependencies start DRAFT; tasks without dependencies start QUEUED unless --draft is given.
|
|
106
106
|
Directly-created Tasks are not wrapped in a synthetic Plan. Planner-generated Tasks are created internally by Relay from planner output.
|
|
107
107
|
|
|
108
108
|
Example:
|
|
@@ -110,7 +110,7 @@ Example:
|
|
|
110
110
|
'task update': `Update mutable durable Task fields in place.
|
|
111
111
|
|
|
112
112
|
Required: --task-id ID and the normal Job storage options.
|
|
113
|
-
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --story-size, --input, --project, --agent, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy.
|
|
113
|
+
Mutable while not RUNNING/SUCCEEDED/CANCELLED: --adapter, --priority, --story-size, --input, --project, --agent, --parent, --deps, --capabilities, --timeout, --max-attempts, --provider, --model, --profile, --reasoning, --cwd, --chatgpt-project, --chatgpt-tab-close-policy, --draft, --queue.
|
|
114
114
|
Use --clear-routing to remove routing metadata.
|
|
115
115
|
|
|
116
116
|
Example:
|
|
@@ -340,6 +340,7 @@ function workerTaskFromArgs(job, args, now) {
|
|
|
340
340
|
const routing = adapter === 'orchestrator' ? undefined : { provider: arg(args, '--provider', 'openai'), model, profile: arg(args, '--profile') || undefined, reasoning: arg(args, '--reasoning') || undefined, cwd: arg(args, '--cwd') || undefined, projectId: arg(args, '--chatgpt-project') || undefined, decidedBy: 'cli', decidedAt: now };
|
|
341
341
|
const closePolicy = hasArg(args, '--chatgpt-tab-close-policy') ? chatGptTabClosePolicy(args) : undefined;
|
|
342
342
|
const size = storySize(args);
|
|
343
|
+
const dependencies = arg(args, '--deps').split(',').filter(Boolean);
|
|
343
344
|
const timeoutMs = adapter === 'orchestrator' ? 0 : hasArg(args, '--timeout') ? integerArg(args, '--timeout', taskOverallTimeoutMs(size, routing?.reasoning)) : taskOverallTimeoutMs(size, routing?.reasoning);
|
|
344
345
|
return {
|
|
345
346
|
jobId: job.id,
|
|
@@ -349,7 +350,7 @@ function workerTaskFromArgs(job, args, now) {
|
|
|
349
350
|
projectName: arg(args, '--project') || undefined,
|
|
350
351
|
agentName: arg(args, '--agent') || undefined,
|
|
351
352
|
parentTaskId: arg(args, '--parent') || null,
|
|
352
|
-
dependencies
|
|
353
|
+
dependencies,
|
|
353
354
|
capabilities: arg(args, '--capabilities').split(',').filter(Boolean),
|
|
354
355
|
adapter,
|
|
355
356
|
chatgptTabClosePolicy: closePolicy,
|
|
@@ -359,7 +360,7 @@ function workerTaskFromArgs(job, args, now) {
|
|
|
359
360
|
routing,
|
|
360
361
|
continuation: continuation(args),
|
|
361
362
|
continuationDeliveredAt: null,
|
|
362
|
-
state: 'QUEUED',
|
|
363
|
+
state: hasArg(args, '--draft') || dependencies.length > 0 ? 'DRAFT' : 'QUEUED',
|
|
363
364
|
attempt: 0,
|
|
364
365
|
maxAttempts: Number(arg(args, '--max-attempts', '3')),
|
|
365
366
|
leaseOwner: null,
|
|
@@ -390,8 +391,13 @@ async function runTaskCommand(action, args) {
|
|
|
390
391
|
task.id = id;
|
|
391
392
|
await store.appendTask(task);
|
|
392
393
|
const refreshed = await store.load(job.id);
|
|
393
|
-
|
|
394
|
-
|
|
394
|
+
const hasActive = refreshed.tasks.some(item => ['QUEUED', 'RUNNING'].includes(item.state));
|
|
395
|
+
const hasBlocked = refreshed.tasks.some(item => item.state === 'BLOCKED');
|
|
396
|
+
const hasDraft = refreshed.tasks.some(item => item.state === 'DRAFT');
|
|
397
|
+
const hasFailed = refreshed.tasks.some(item => item.state === 'FAILED');
|
|
398
|
+
const nextState = hasActive ? 'RUNNING' : hasBlocked ? 'BLOCKED' : hasDraft ? 'WAITING' : hasFailed ? 'FAILED' : refreshed.state;
|
|
399
|
+
if (refreshed.state !== nextState) {
|
|
400
|
+
refreshed.state = nextState;
|
|
395
401
|
refreshed.updatedAt = now;
|
|
396
402
|
await store.saveJob(refreshed);
|
|
397
403
|
}
|
|
@@ -456,6 +462,16 @@ async function runTaskCommand(action, args) {
|
|
|
456
462
|
task.chatgptTabClosePolicy = chatGptTabClosePolicy(args);
|
|
457
463
|
task.timeoutMs = integerArg(args, '--timeout', task.timeoutMs);
|
|
458
464
|
task.maxAttempts = integerArg(args, '--max-attempts', task.maxAttempts);
|
|
465
|
+
if (hasArg(args, '--draft') && hasArg(args, '--queue'))
|
|
466
|
+
throw new Error('--draft and --queue are mutually exclusive');
|
|
467
|
+
if (hasArg(args, '--draft'))
|
|
468
|
+
transitionTask(task, 'DRAFT');
|
|
469
|
+
if (hasArg(args, '--queue')) {
|
|
470
|
+
const taskMap = new Map(job.tasks.map(item => [item.id, item]));
|
|
471
|
+
if (!task.dependencies.every(dep => taskMap.get(dep)?.state === 'SUCCEEDED'))
|
|
472
|
+
throw new Error('Task dependencies must succeed before queueing');
|
|
473
|
+
transitionTask(task, 'QUEUED');
|
|
474
|
+
}
|
|
459
475
|
if (hasArg(args, '--max-attempts') && task.maxAttempts < task.attempt)
|
|
460
476
|
throw new Error('--max-attempts cannot be less than the current attempt count');
|
|
461
477
|
const routingFlags = ['--provider', '--model', '--profile', '--reasoning', '--cwd', '--chatgpt-project'];
|
|
@@ -508,7 +524,7 @@ async function runJobCommand(action, args) {
|
|
|
508
524
|
throw new Error(usage);
|
|
509
525
|
const repo = repository(args);
|
|
510
526
|
if (action === 'create') {
|
|
511
|
-
const taskOnlyFlags = ['--start', '--task-id', '--input', '--adapter', '--provider', '--model', '--profile', '--reasoning', '--story-size', '--project', '--agent', '--parent', '--deps', '--capabilities', '--attachments', '--max-attempts', '--timeout', '--cwd', '--chatgpt-project', '--chatgpt-tab-close-policy', '--output', '--output-path'];
|
|
527
|
+
const taskOnlyFlags = ['--start', '--task-id', '--input', '--adapter', '--provider', '--model', '--profile', '--reasoning', '--story-size', '--project', '--agent', '--parent', '--deps', '--capabilities', '--attachments', '--max-attempts', '--timeout', '--cwd', '--chatgpt-project', '--chatgpt-tab-close-policy', '--output', '--output-path', '--draft', '--queue'];
|
|
512
528
|
const invalid = taskOnlyFlags.find(flag => hasArg(args, flag));
|
|
513
529
|
if (invalid)
|
|
514
530
|
throw new Error(`job create does not accept task execution option ${invalid}; create the job first, then let the orchestrator create the initial plan/task batch`);
|
package/dist/dashboard.js
CHANGED
|
@@ -95,7 +95,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
95
95
|
.job-meta span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
96
96
|
.status-pill,.badge { display:inline-flex; align-items:center; border-radius:999px; padding:.06rem .42rem; min-height:1.35rem; font-size:.68rem; font-weight:800; }
|
|
97
97
|
.status-pill.ok,.badge.RUNNING,.badge.SUCCEEDED,.badge.COMPLETED { background:#e5f3e9; color:var(--ok); }
|
|
98
|
-
.status-pill.warn,.badge.BLOCKED,.badge.
|
|
98
|
+
.status-pill.warn,.badge.BLOCKED,.badge.DRAFT,.badge.QUEUED,.badge.WAITING,.badge.OPEN { background:#fff1d2; color:var(--warn); }
|
|
99
99
|
.status-pill.bad,.badge.FAILED { background:#ffe8e8; color:var(--bad); }
|
|
100
100
|
.status-pill.neutral,.badge.CANCELLED { background:#edf0f4; color:var(--muted); }
|
|
101
101
|
.badge.DRAFT { background:#eef2ff; color:#3f51b5; }
|
|
@@ -220,7 +220,7 @@ function markdownField(label,input){const box=element('div','detail-group');box.
|
|
|
220
220
|
function rememberSelection(){if(selectedJob)sessionStorage.setItem('agents-relay:selected-job',selectedJob);else sessionStorage.removeItem('agents-relay:selected-job');if(selectedTaskId)sessionStorage.setItem('agents-relay:selected-task',selectedTaskId);else sessionStorage.removeItem('agents-relay:selected-task');}
|
|
221
221
|
function selected(){return jobs.find(job=>job.id===selectedJob)||jobs[0]||null;}
|
|
222
222
|
function counts(tasks){const out={};for(const task of Array.isArray(tasks)?tasks:[])out[task.state]=(out[task.state]||0)+1;return out;}
|
|
223
|
-
function compactCounts(input){const order=['RUNNING','BLOCKED','FAILED','QUEUED','
|
|
223
|
+
function compactCounts(input){const order=['RUNNING','BLOCKED','FAILED','QUEUED','DRAFT','SUCCEEDED','CANCELLED'];return order.filter(state=>input[state]).map(state=>input[state]+' '+state).join(', ')||'No tasks';}
|
|
224
224
|
function awaitingInitialTask(job){return job?.state==='OPEN'&&Array.isArray(job?.tasks)&&job.tasks.length===0;}
|
|
225
225
|
function relativeTime(timestamp){if(!timestamp)return '—';const parsed=Date.parse(timestamp);if(!Number.isFinite(parsed))return value(timestamp);const seconds=Math.max(0,Math.round((Date.now()-parsed)/1000));if(seconds<60)return 'just now';if(seconds<3600)return Math.floor(seconds/60)+'m ago';if(seconds<86400)return Math.floor(seconds/3600)+'h ago';return Math.floor(seconds/86400)+'d ago';}
|
|
226
226
|
function safePrUrl(repository,prNumber){if(typeof repository!=='string'||!Number.isInteger(prNumber)||prNumber<=0)return null;const parts=repository.split('/');if(parts.length!==2)return null;const [owner,repo]=parts;if(!owner||!repo||owner==='.'||owner==='..'||repo==='.'||repo==='..')return null;if(!/^[A-Za-z0-9-]+$/.test(owner)||!/^[A-Za-z0-9._-]+$/.test(repo))return null;return 'https://github.com/'+owner+'/'+repo+'/pull/'+prNumber;}
|
package/dist/pool.js
CHANGED
|
@@ -7,7 +7,7 @@ export function needsLifecycleReconciliation(job, now = Date.now()) {
|
|
|
7
7
|
// Create-only Jobs stay OPEN until a caller explicitly creates work.
|
|
8
8
|
if (job.tasks.length === 0)
|
|
9
9
|
return false;
|
|
10
|
-
if (job.tasks.some(task =>
|
|
10
|
+
if (job.tasks.some(task => task.state === 'QUEUED' || (task.state === 'DRAFT' && task.dependencies.length > 0)))
|
|
11
11
|
return true;
|
|
12
12
|
return job.tasks.some(task => task.state === 'RUNNING' && task.leaseExpiresAt !== null && new Date(task.leaseExpiresAt).getTime() <= now);
|
|
13
13
|
}
|
|
@@ -48,7 +48,7 @@ export class RepositoryWorkerPool {
|
|
|
48
48
|
if (capacity <= 0)
|
|
49
49
|
break;
|
|
50
50
|
const store = new GitHubStore(this.client, job.repository, job.prNumber, this.trustedAuthors);
|
|
51
|
-
const ready = job.tasks.filter(task =>
|
|
51
|
+
const ready = job.tasks.filter(task => task.state === 'QUEUED' || (task.state === 'DRAFT' && task.dependencies.length > 0)).length;
|
|
52
52
|
const expiredRunning = job.tasks.some(task => task.state === 'RUNNING' && task.leaseExpiresAt !== null && new Date(task.leaseExpiresAt).getTime() <= Date.now());
|
|
53
53
|
if (!needsLifecycleReconciliation(job))
|
|
54
54
|
continue;
|
package/dist/reconciler.js
CHANGED
|
@@ -199,7 +199,7 @@ export class Reconciler {
|
|
|
199
199
|
return;
|
|
200
200
|
job.review = { ...job.review, outcome: 'stale', updatedAt: new Date().toISOString() };
|
|
201
201
|
const staleTask = job.review.taskId ? job.tasks.find(task => task.id === job.review?.taskId) : undefined;
|
|
202
|
-
if (staleTask && ['
|
|
202
|
+
if (staleTask && ['DRAFT', 'QUEUED', 'RUNNING'].includes(staleTask.state)) {
|
|
203
203
|
this.live.get(staleTask.id)?.execution.cancel();
|
|
204
204
|
transitionTask(staleTask, 'CANCELLED');
|
|
205
205
|
staleTask.error = 'Review evidence became stale after the PR head changed';
|
|
@@ -218,7 +218,7 @@ export class Reconciler {
|
|
|
218
218
|
if (review?.headSha === headSha && ['started', 'approved', 'changes_required', 'blocked'].includes(review.outcome))
|
|
219
219
|
return;
|
|
220
220
|
const reviewTasks = job.tasks.filter(task => task.agentName === 'reviewer' && task.reviewHeadSha === headSha);
|
|
221
|
-
if (reviewTasks.some(task => ['
|
|
221
|
+
if (reviewTasks.some(task => ['DRAFT', 'QUEUED', 'RUNNING'].includes(task.state)))
|
|
222
222
|
return;
|
|
223
223
|
if (reviewTasks.some(task => task.state === 'SUCCEEDED' && review?.headSha === headSha && review.outcome === 'approved'))
|
|
224
224
|
return;
|
|
@@ -391,7 +391,7 @@ export class Reconciler {
|
|
|
391
391
|
const slot = job.schedule?.currentRunSlot ? Date.parse(job.schedule.currentRunSlot) : Number.NaN;
|
|
392
392
|
const scopedTasks = Number.isFinite(slot) ? job.tasks.filter(task => Date.parse(task.createdAt) >= slot) : job.tasks;
|
|
393
393
|
const planners = scopedTasks.filter(task => task.kind === 'planner').sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
394
|
-
if (planners.some(task => ['
|
|
394
|
+
if (planners.some(task => ['DRAFT', 'QUEUED', 'RUNNING'].includes(task.state)))
|
|
395
395
|
return job;
|
|
396
396
|
if (scopedTasks.some(task => task.state === 'FAILED' || task.state === 'BLOCKED'))
|
|
397
397
|
return job;
|
|
@@ -645,7 +645,7 @@ export class Reconciler {
|
|
|
645
645
|
}
|
|
646
646
|
plannerChild(job, planner, spec) {
|
|
647
647
|
const now = new Date().toISOString();
|
|
648
|
-
return { jobId: job.id, id: spec.id, kind: 'work', priority: spec.priority ?? job.priority, storySize: spec.storySize ?? 'M', projectName: spec.projectName, agentName: spec.agentName, parentTaskId: planner.id, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], workerRequirements: spec.workerRequirements, adapter: 'codex', input: spec.input, output: spec.output, continuation: spec.continuation ?? null, continuationDeliveredAt: null, state: 'QUEUED', 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: planner.id, planningSource: planner.planningSource ?? 'model-planner', planningRound: planner.planningRound ?? 1 };
|
|
648
|
+
return { jobId: job.id, id: spec.id, kind: 'work', priority: spec.priority ?? job.priority, storySize: spec.storySize ?? 'M', projectName: spec.projectName, agentName: spec.agentName, parentTaskId: planner.id, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], workerRequirements: spec.workerRequirements, adapter: 'codex', input: spec.input, output: spec.output, continuation: spec.continuation ?? null, continuationDeliveredAt: null, state: (spec.dependencies?.length ?? 0) > 0 ? 'DRAFT' : 'QUEUED', 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: planner.id, planningSource: planner.planningSource ?? 'model-planner', planningRound: planner.planningRound ?? 1 };
|
|
649
649
|
}
|
|
650
650
|
clearExecution(taskId, executionId) {
|
|
651
651
|
const live = this.live.get(taskId);
|
package/dist/scheduled-jobs.js
CHANGED
|
@@ -147,7 +147,7 @@ export async function runScheduledBucket(client, repository, trustedAuthors, ref
|
|
|
147
147
|
await syncScheduleLabels(client, repository, job);
|
|
148
148
|
return { job, status: 'skipped-idempotent' };
|
|
149
149
|
}
|
|
150
|
-
const active = job.state === 'RUNNING' || job.tasks.some(task => ['
|
|
150
|
+
const active = job.state === 'RUNNING' || job.tasks.some(task => ['QUEUED', 'RUNNING'].includes(task.state));
|
|
151
151
|
if (active && schedule.concurrencyPolicy === 'skip') {
|
|
152
152
|
schedule.lastScheduledSlot = slot.toISOString();
|
|
153
153
|
schedule.nextRunAt = nextScheduleRun(schedule, now).toISOString();
|
package/dist/scheduler.js
CHANGED
|
@@ -32,12 +32,17 @@ export function schedule(job) {
|
|
|
32
32
|
}
|
|
33
33
|
const map = new Map(job.tasks.map(t => [t.id, t]));
|
|
34
34
|
for (const task of scopedTasks) {
|
|
35
|
-
|
|
35
|
+
const ready = dependencyReady(task, map);
|
|
36
|
+
if (task.state === 'DRAFT' && task.dependencies.length > 0 && ready)
|
|
37
|
+
transitionTask(task, 'QUEUED');
|
|
38
|
+
else if (task.state === 'QUEUED' && !ready)
|
|
39
|
+
transitionTask(task, 'DRAFT');
|
|
40
|
+
else if (task.state === 'QUEUED' && ready && task.error !== null)
|
|
36
41
|
transitionTask(task, 'QUEUED');
|
|
37
42
|
}
|
|
38
43
|
const active = scopedTasks.some(t => ['QUEUED', 'RUNNING'].includes(t.state));
|
|
39
44
|
const blocked = scopedTasks.some(t => t.state === 'BLOCKED');
|
|
40
|
-
const waiting = scopedTasks.some(t => t.state === '
|
|
45
|
+
const waiting = scopedTasks.some(t => t.state === 'DRAFT');
|
|
41
46
|
const failed = scopedTasks.some(t => t.state === 'FAILED');
|
|
42
47
|
const cancelled = scopedTasks.every(t => t.state === 'CANCELLED');
|
|
43
48
|
const next = active ? 'RUNNING' : failed ? 'FAILED' : blocked ? 'BLOCKED' : waiting ? 'WAITING' : cancelled ? 'CANCELLED' : completionGateSatisfied(job) ? 'COMPLETED' : 'WAITING';
|
package/dist/types.js
CHANGED
|
@@ -3,7 +3,7 @@ export function storySizeTimeoutMs(size) { return STORY_SIZE_TIMEOUT_MS[size]; }
|
|
|
3
3
|
export function taskOverallTimeoutMs(size, reasoning) { return storySizeTimeoutMs(size) * (reasoning?.toLowerCase() === 'high' ? 2 : 1); }
|
|
4
4
|
export function chatGptStartAckTimeoutMs(overallTimeoutMs) { return Math.min(600000, Math.max(120000, Math.floor(overallTimeoutMs / 3))); }
|
|
5
5
|
const jobTransitions = { OPEN: ['RUNNING', 'WAITING', 'BLOCKED', 'COMPLETED', 'CANCELLED'], RUNNING: ['OPEN', 'WAITING', 'BLOCKED', 'COMPLETED', 'FAILED', 'CANCELLED'], WAITING: ['RUNNING', 'BLOCKED', 'COMPLETED', 'CANCELLED'], BLOCKED: ['RUNNING', 'COMPLETED', 'CANCELLED'], COMPLETED: ['RUNNING', 'CANCELLED'], FAILED: ['RUNNING', 'COMPLETED', 'CANCELLED'], CANCELLED: ['RUNNING', 'COMPLETED'] };
|
|
6
|
-
const taskTransitions = {
|
|
6
|
+
const taskTransitions = { DRAFT: ['QUEUED', 'BLOCKED', 'CANCELLED'], QUEUED: ['DRAFT', 'RUNNING', 'BLOCKED', 'FAILED', 'CANCELLED'], RUNNING: ['BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED', 'QUEUED'], BLOCKED: ['QUEUED', 'CANCELLED'], SUCCEEDED: [], FAILED: ['QUEUED', 'RUNNING', 'CANCELLED'], CANCELLED: [] };
|
|
7
7
|
export function assertJobTransition(from, to) { if (from !== to && !jobTransitions[from].includes(to))
|
|
8
8
|
throw new Error(`Invalid job transition ${from} -> ${to}`); }
|
|
9
9
|
export function assertTaskTransition(from, to) { if (from !== to && !taskTransitions[from].includes(to))
|
package/package.json
CHANGED
|
@@ -30,6 +30,18 @@ Relay derives the overall execution timeout from `storySize`. If `routing.reason
|
|
|
30
30
|
|
|
31
31
|
Raw `timeoutMs` is a compatibility/debug/hotfix override, not the normal task-production interface.
|
|
32
32
|
|
|
33
|
+
## Task readiness states
|
|
34
|
+
|
|
35
|
+
Task readiness is explicit and must use these semantics:
|
|
36
|
+
|
|
37
|
+
- `DRAFT`: durable Task exists but is not executable now. A DRAFT Task with dependencies is promoted to `QUEUED` when all dependencies succeed. A DRAFT Task with no dependencies remains held until an explicit release to `QUEUED`.
|
|
38
|
+
- `QUEUED`: executable now; Relay may launch it immediately subject only to worker capacity/routing.
|
|
39
|
+
- `RUNNING`: worker execution has actually started.
|
|
40
|
+
- `BLOCKED`: execution cannot continue without an external resolution or intervention. Once a Task has entered `RUNNING`, it must not return to `DRAFT`; waiting discovered during execution is `BLOCKED`.
|
|
41
|
+
- `SUCCEEDED`, `FAILED`, and `CANCELLED` are terminal outcomes, except that retry explicitly requeues a failed/blocked Task.
|
|
42
|
+
|
|
43
|
+
Do not use `WAITING` as a Task state. `WAITING` remains a Job state for an active Job that is intentionally awaiting a future trigger, schedule slot, or condition.
|
|
44
|
+
|
|
33
45
|
## Worker lifecycle
|
|
34
46
|
|
|
35
47
|
Relay owns process lifecycle. Workers own semantic task lifecycle. A worker must publish canonical `task.started` as its first execution action, then exactly one of `task.completed`, `task.failed`, `task.blocked`, or `task.cancelled`. Process launch/exit events do not substitute for worker semantic events.
|