agents-relay 1.0.29 → 1.0.30
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
CHANGED
|
@@ -38,7 +38,7 @@ autonomous runtime uses the bundled `skills/agents-relay/agents/planner.agent.md
|
|
|
38
38
|
snapshot as JSON on stdin and must return `{ "objective_status": "in_progress"|"satisfied", "assessment": "...", "next_tasks": [...] }`.
|
|
39
39
|
Planner-created tasks use stable IDs and the existing parent/subtask tree, so
|
|
40
40
|
retries and daemon restarts do not create duplicate work.
|
|
41
|
-
Task producers must follow `skills/agents-relay/contracts/task-job-contract.md`; that shared skill contract defines story size,
|
|
41
|
+
Task producers must follow `skills/agents-relay/contracts/task-job-contract.md`; that shared skill contract defines story size, lifecycle events, review, and auto-merge semantics.
|
|
42
42
|
All task producers use the same story-size contract: `S`≈5 minutes, `M`≈15 minutes, and `L`≈45 minutes of normal-reasoning execution, with `M` as the default. `high` reasoning doubles that overall window. ChatGPT Browser Worker start-ack timeout is derived as one third of the effective overall timeout, clamped between 1 and 5 minutes. Orchestrators and planners should choose `storySize`, not raw timeout milliseconds; `--timeout` remains only as an explicit compatibility/debug override.
|
|
43
43
|
|
|
44
44
|
job create first resolves an existing open PR with the same head/base, then creates it only when needed. Re-running it is idempotent: it reuses the PR and maintains exactly one trusted agents-relay:job:v1 marker.
|
package/dist/reconciler.js
CHANGED
|
@@ -71,7 +71,7 @@ export class Reconciler {
|
|
|
71
71
|
}
|
|
72
72
|
async handleEvent(event) {
|
|
73
73
|
if (event.type === 'task.started') {
|
|
74
|
-
this.markWorkerStarted(event);
|
|
74
|
+
await this.markWorkerStarted(event);
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
if (['task.completed', 'task.failed', 'task.blocked', 'task.cancelled'].includes(event.type)) {
|
|
@@ -512,17 +512,44 @@ export class Reconciler {
|
|
|
512
512
|
clearTimeout(graceTimer);
|
|
513
513
|
this.terminalExitGraceTimers.delete(taskId);
|
|
514
514
|
}
|
|
515
|
-
markWorkerStarted(event) {
|
|
516
|
-
const live = this.live.get(event.task_id);
|
|
517
|
-
if (!live || event.job_id !== live.jobId)
|
|
518
|
-
return;
|
|
515
|
+
async markWorkerStarted(event) {
|
|
519
516
|
const sourceAgent = typeof event.source?.agent === 'string' ? event.source.agent : '';
|
|
520
517
|
const sourceComponent = typeof event.source?.component === 'string' ? event.source.component : '';
|
|
521
518
|
if (sourceAgent === 'relay' || sourceComponent === 'agents-relay')
|
|
522
519
|
return;
|
|
523
|
-
|
|
520
|
+
const live = this.live.get(event.task_id);
|
|
521
|
+
if (live && event.job_id === live.jobId) {
|
|
522
|
+
if (event.timestamp && new Date(event.timestamp).getTime() < live.launchBarrier)
|
|
523
|
+
return;
|
|
524
|
+
live.workerStarted = true;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const job = await this.store.load(event.job_id);
|
|
528
|
+
const task = job.tasks.find(item => item.id === event.task_id);
|
|
529
|
+
if (!task || task.state !== 'FAILED' || !this.isRecoverableWatchdogFailure(task))
|
|
524
530
|
return;
|
|
525
|
-
|
|
531
|
+
if (event.timestamp && new Date(event.timestamp).getTime() < new Date(task.updatedAt).getTime())
|
|
532
|
+
return;
|
|
533
|
+
transitionTask(task, 'RUNNING');
|
|
534
|
+
task.failureReason = undefined;
|
|
535
|
+
task.error = null;
|
|
536
|
+
task.updatedAt = event.timestamp || new Date().toISOString();
|
|
537
|
+
await this.store.saveTask(task);
|
|
538
|
+
this.detach(this.reconcile(job.id), job.id, task.id, 'late worker-start reconciliation');
|
|
539
|
+
}
|
|
540
|
+
isRecoverableWatchdogFailure(task) {
|
|
541
|
+
if (['worker_start_timeout', 'terminal_event_timeout', 'worker_exit_before_start', 'worker_exit_without_terminal_event'].includes(task.failureReason ?? ''))
|
|
542
|
+
return true;
|
|
543
|
+
const error = task.error ?? '';
|
|
544
|
+
return error.includes('worker_start_timeout')
|
|
545
|
+
|| error.includes('timed out waiting for the mandatory terminal worker event')
|
|
546
|
+
|| error.includes('exited before publishing task.started')
|
|
547
|
+
|| error.includes('exited after task.started without publishing a terminal task event');
|
|
548
|
+
}
|
|
549
|
+
isWorkerOwnedEvent(event) {
|
|
550
|
+
const sourceAgent = typeof event.source?.agent === 'string' ? event.source.agent : '';
|
|
551
|
+
const sourceComponent = typeof event.source?.component === 'string' ? event.source.component : '';
|
|
552
|
+
return sourceAgent !== 'relay' && sourceComponent !== 'agents-relay';
|
|
526
553
|
}
|
|
527
554
|
async failLaunch(jobId, taskId, executionId, message, phase = 'launch') {
|
|
528
555
|
const claim = executionId ?? `${jobId}:${taskId}`;
|
|
@@ -592,7 +619,16 @@ export class Reconciler {
|
|
|
592
619
|
async applyTerminalEvent(event) {
|
|
593
620
|
const job = await this.store.load(event.job_id);
|
|
594
621
|
const task = job.tasks.find(item => item.id === event.task_id);
|
|
595
|
-
if (!task || !['codex', 'chatgpt'].includes(task.adapter)
|
|
622
|
+
if (!task || !['codex', 'chatgpt'].includes(task.adapter))
|
|
623
|
+
return;
|
|
624
|
+
if (task.state === 'FAILED' && this.isRecoverableWatchdogFailure(task) && this.isWorkerOwnedEvent(event)) {
|
|
625
|
+
if (event.timestamp && new Date(event.timestamp).getTime() < new Date(task.updatedAt).getTime())
|
|
626
|
+
return;
|
|
627
|
+
transitionTask(task, 'RUNNING');
|
|
628
|
+
task.failureReason = undefined;
|
|
629
|
+
task.error = null;
|
|
630
|
+
}
|
|
631
|
+
if (task.state !== 'RUNNING')
|
|
596
632
|
return;
|
|
597
633
|
const terminalWaiter = this.terminalWaiters.get(task.id);
|
|
598
634
|
if (terminalWaiter && (!event.timestamp || new Date(event.timestamp).getTime() < terminalWaiter.launchBarrier))
|
|
@@ -606,12 +642,14 @@ export class Reconciler {
|
|
|
606
642
|
if (event.type === 'task.completed') {
|
|
607
643
|
task.result = { summary: event.message || `Task ${task.id} completed`, data: event.data };
|
|
608
644
|
task.error = null;
|
|
645
|
+
task.failureReason = undefined;
|
|
609
646
|
transitionTask(task, 'SUCCEEDED');
|
|
610
647
|
await this.store.saveTask(task);
|
|
611
648
|
await this.deliverContinuation(job, task);
|
|
612
649
|
}
|
|
613
650
|
else if (event.type === 'task.failed') {
|
|
614
651
|
task.error = event.message || `Task ${task.id} failed`;
|
|
652
|
+
task.failureReason = typeof event.data?.reason === 'string' ? event.data.reason : undefined;
|
|
615
653
|
transitionTask(task, 'FAILED');
|
|
616
654
|
await this.store.saveTask(task);
|
|
617
655
|
}
|
package/dist/types.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const STORY_SIZE_TIMEOUT_MS = { S: 300000, M: 900000, L: 2700000 };
|
|
2
2
|
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
|
-
export function chatGptStartAckTimeoutMs(overallTimeoutMs) { return Math.min(
|
|
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', 'CANCELLED'], COMPLETED: ['RUNNING', 'CANCELLED'], FAILED: ['RUNNING', 'CANCELLED'], CANCELLED: ['RUNNING'] };
|
|
6
|
-
const taskTransitions = { QUEUED: ['READY', 'BLOCKED', 'CANCELLED'], READY: ['RUNNING', 'BLOCKED', 'FAILED', 'CANCELLED'], RUNNING: ['WAITING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED', 'READY'], WAITING: ['READY', 'BLOCKED', 'CANCELLED'], BLOCKED: ['READY', 'CANCELLED'], SUCCEEDED: [], FAILED: ['READY', 'CANCELLED'], CANCELLED: [] };
|
|
6
|
+
const taskTransitions = { QUEUED: ['READY', 'BLOCKED', 'CANCELLED'], READY: ['RUNNING', 'BLOCKED', 'FAILED', 'CANCELLED'], RUNNING: ['WAITING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED', 'READY'], WAITING: ['READY', 'BLOCKED', 'CANCELLED'], BLOCKED: ['READY', 'CANCELLED'], SUCCEEDED: [], FAILED: ['READY', '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
|
@@ -17,11 +17,7 @@ Every worker task has one of three story sizes:
|
|
|
17
17
|
|
|
18
18
|
Relay derives the overall execution timeout from `storySize`. If `routing.reasoning` is `high`, Relay doubles the overall execution timeout. Other reasoning levels do not change it.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`startAckTimeout = clamp(overallTimeout / 3, 1 minute, 5 minutes)`
|
|
23
|
-
|
|
24
|
-
This applies equally to normal ChatGPT tasks and reviewer tasks. Raw `timeoutMs` is a compatibility/debug/hotfix override, not the normal task-production interface.
|
|
20
|
+
Raw `timeoutMs` is a compatibility/debug/hotfix override, not the normal task-production interface.
|
|
25
21
|
|
|
26
22
|
## Worker lifecycle
|
|
27
23
|
|