agents-relay 1.0.22 → 1.0.23
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/dist/planner.js
CHANGED
|
@@ -21,6 +21,30 @@ function stringArray(value, field, fallback = []) {
|
|
|
21
21
|
throw new Error(`Planner ${field} must be an array of non-empty strings`);
|
|
22
22
|
return [...value];
|
|
23
23
|
}
|
|
24
|
+
function rawValue(value) {
|
|
25
|
+
if (typeof value === 'string')
|
|
26
|
+
return JSON.stringify(value);
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
return 'undefined';
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(value) ?? String(value);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function normalizePlannerPriority(value, field) {
|
|
37
|
+
if (typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 3)
|
|
38
|
+
return `P${value}`;
|
|
39
|
+
if (typeof value === 'string') {
|
|
40
|
+
const normalized = value.trim();
|
|
41
|
+
if (/^P[0-3]$/i.test(normalized))
|
|
42
|
+
return normalized.toUpperCase();
|
|
43
|
+
if (/^[0-3]$/.test(normalized))
|
|
44
|
+
return `P${normalized}`;
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Planner ${field} is invalid: ${rawValue(value)}`);
|
|
47
|
+
}
|
|
24
48
|
function routing(value, field) {
|
|
25
49
|
if (value === undefined || value === null)
|
|
26
50
|
return undefined;
|
|
@@ -58,8 +82,7 @@ export function parsePlannerResult(value) {
|
|
|
58
82
|
throw new Error(`Planner next_tasks[${index}].output is required`);
|
|
59
83
|
if (output.kind === 'file' && (typeof output.path !== 'string' || output.path.length === 0))
|
|
60
84
|
throw new Error(`Planner next_tasks[${index}].output.path is required for file output`);
|
|
61
|
-
|
|
62
|
-
throw new Error(`Planner next_tasks[${index}].priority is invalid`);
|
|
85
|
+
const priority = task.priority === undefined ? undefined : normalizePlannerPriority(task.priority, `next_tasks[${index}].priority`);
|
|
63
86
|
const maxAttempts = task.maxAttempts === undefined ? 3 : task.maxAttempts;
|
|
64
87
|
const timeoutMs = task.timeoutMs === undefined ? 300000 : task.timeoutMs;
|
|
65
88
|
if (typeof maxAttempts !== 'number' || !Number.isInteger(maxAttempts) || maxAttempts < 1)
|
|
@@ -67,7 +90,7 @@ export function parsePlannerResult(value) {
|
|
|
67
90
|
if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs < 0)
|
|
68
91
|
throw new Error(`Planner next_tasks[${index}].timeoutMs is invalid`);
|
|
69
92
|
return {
|
|
70
|
-
id, input: task.input, priority
|
|
93
|
+
id, input: task.input, priority,
|
|
71
94
|
projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'),
|
|
72
95
|
dependencies: stringArray(task.dependencies, 'dependencies'), capabilities: stringArray(task.capabilities, 'capabilities'),
|
|
73
96
|
adapter: adapter, output: output, routing: routing(task.routing, `next_tasks[${index}].routing`),
|
package/dist/reconciler.js
CHANGED
|
@@ -59,6 +59,7 @@ export class Reconciler {
|
|
|
59
59
|
live = new Map();
|
|
60
60
|
plannerLive = new Map();
|
|
61
61
|
terminalTimers = new Map();
|
|
62
|
+
terminalExitGraceTimers = new Map();
|
|
62
63
|
continuationLive = new Set();
|
|
63
64
|
terminalWaiters = new Map();
|
|
64
65
|
reconciling = false;
|
|
@@ -67,6 +68,10 @@ export class Reconciler {
|
|
|
67
68
|
this.options = options;
|
|
68
69
|
}
|
|
69
70
|
async handleEvent(event) {
|
|
71
|
+
if (event.type === 'task.started') {
|
|
72
|
+
this.markWorkerStarted(event);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
70
75
|
if (['task.completed', 'task.failed', 'task.blocked', 'task.cancelled'].includes(event.type)) {
|
|
71
76
|
await this.applyTerminalEvent(event);
|
|
72
77
|
return;
|
|
@@ -341,7 +346,7 @@ export class Reconciler {
|
|
|
341
346
|
void (execution.launched ?? Promise.resolve()).then(() => this.emit(eventFor(job.id, task.id, task.parentTaskId, 'task.process.launched', 'running', `Task ${task.id} process launched`, 'user', { adapter: adapter.id, model: task.routing?.model }, { agent: 'relay', component: 'agents-relay' }))).catch(error => this.failLaunch(job.id, task.id, durableExecutionId, error instanceof Error ? error.message : String(error), 'launch'));
|
|
342
347
|
if (startAckSetup)
|
|
343
348
|
void startAckSetup.ack.finally(() => { void startAckSetup.stop(); }).catch(() => { });
|
|
344
|
-
this.live.set(task.id, { taskId: task.id, executionId: durableExecutionId, execution, controller });
|
|
349
|
+
this.live.set(task.id, { jobId: job.id, taskId: task.id, executionId: durableExecutionId, execution, controller, launchBarrier: Date.now(), workerStarted: false });
|
|
345
350
|
const terminalTimer = setTimeout(() => {
|
|
346
351
|
this.detach(this.timeoutTask(job.id, task.id, durableExecutionId), job.id, task.id, 'worker terminal-event timeout');
|
|
347
352
|
}, Math.max(1, task.timeoutMs));
|
|
@@ -484,6 +489,22 @@ export class Reconciler {
|
|
|
484
489
|
if (timer)
|
|
485
490
|
clearTimeout(timer);
|
|
486
491
|
this.terminalTimers.delete(taskId);
|
|
492
|
+
const graceTimer = this.terminalExitGraceTimers.get(taskId);
|
|
493
|
+
if (graceTimer)
|
|
494
|
+
clearTimeout(graceTimer);
|
|
495
|
+
this.terminalExitGraceTimers.delete(taskId);
|
|
496
|
+
}
|
|
497
|
+
markWorkerStarted(event) {
|
|
498
|
+
const live = this.live.get(event.task_id);
|
|
499
|
+
if (!live || event.job_id !== live.jobId)
|
|
500
|
+
return;
|
|
501
|
+
const sourceAgent = typeof event.source?.agent === 'string' ? event.source.agent : '';
|
|
502
|
+
const sourceComponent = typeof event.source?.component === 'string' ? event.source.component : '';
|
|
503
|
+
if (sourceAgent === 'relay' || sourceComponent === 'agents-relay')
|
|
504
|
+
return;
|
|
505
|
+
if (event.timestamp && new Date(event.timestamp).getTime() < live.launchBarrier)
|
|
506
|
+
return;
|
|
507
|
+
live.workerStarted = true;
|
|
487
508
|
}
|
|
488
509
|
async failLaunch(jobId, taskId, executionId, message, phase = 'launch') {
|
|
489
510
|
const job = await this.store.load(jobId);
|
|
@@ -496,13 +517,37 @@ export class Reconciler {
|
|
|
496
517
|
}
|
|
497
518
|
async runtimeSettled(jobId, taskId, executionId, result, error) {
|
|
498
519
|
const live = this.live.get(taskId);
|
|
520
|
+
const workerStarted = live?.executionId === executionId && live.workerStarted;
|
|
499
521
|
if (live?.executionId === executionId)
|
|
500
522
|
this.live.delete(taskId);
|
|
501
523
|
const job = await this.store.load(jobId);
|
|
502
524
|
const task = job.tasks.find(item => item.id === taskId);
|
|
503
|
-
if (!task || task.executionId !== executionId
|
|
525
|
+
if (!task || task.executionId !== executionId)
|
|
504
526
|
return;
|
|
505
527
|
await this.emit(eventFor(jobId, taskId, task.parentTaskId, task.adapter === 'chatgpt' ? 'task.process.async_exited' : 'task.process.exited', error ? 'failed' : 'succeeded', error ?? result?.summary ?? 'Worker runtime exited', 'orchestrator', { authoritative: false, waitingForTerminalEvent: true, processError: error }, { agent: 'relay', component: 'agents-relay' }));
|
|
528
|
+
if (task.state === 'RUNNING' && task.adapter === 'codex')
|
|
529
|
+
this.scheduleUnexpectedExit(jobId, taskId, executionId, workerStarted === true);
|
|
530
|
+
}
|
|
531
|
+
scheduleUnexpectedExit(jobId, taskId, executionId, workerStarted) {
|
|
532
|
+
const graceMs = Math.max(1, this.options.terminalExitGraceMs ?? 1000);
|
|
533
|
+
const timer = setTimeout(() => {
|
|
534
|
+
this.terminalExitGraceTimers.delete(taskId);
|
|
535
|
+
this.detach(this.failUnexpectedExit(jobId, taskId, executionId, workerStarted), jobId, taskId, 'unexpected worker process exit');
|
|
536
|
+
}, graceMs);
|
|
537
|
+
this.terminalExitGraceTimers.set(taskId, timer);
|
|
538
|
+
}
|
|
539
|
+
async failUnexpectedExit(jobId, taskId, executionId, workerStarted) {
|
|
540
|
+
const job = await this.store.load(jobId);
|
|
541
|
+
const task = job.tasks.find(item => item.id === taskId);
|
|
542
|
+
if (!task || task.executionId !== executionId || task.state !== 'RUNNING')
|
|
543
|
+
return;
|
|
544
|
+
const reason = workerStarted ? 'worker_exit_without_terminal_event' : 'worker_exit_before_start';
|
|
545
|
+
const message = workerStarted
|
|
546
|
+
? `Worker ${taskId} exited after task.started without publishing a terminal task event`
|
|
547
|
+
: `Worker ${taskId} exited before publishing task.started`;
|
|
548
|
+
const event = eventFor(jobId, taskId, task.parentTaskId, 'task.failed', 'failed', message, 'user', { reason, terminalGraceMs: this.options.terminalExitGraceMs ?? 1000 }, { agent: 'relay', component: 'agents-relay', watchdog: true });
|
|
549
|
+
await this.applyTerminalEvent(event);
|
|
550
|
+
await this.emit(event);
|
|
506
551
|
}
|
|
507
552
|
async timeoutTask(jobId, taskId, executionId) {
|
|
508
553
|
const live = this.live.get(taskId);
|
package/package.json
CHANGED
|
@@ -47,6 +47,8 @@ Agents Relay maps those terminal events to durable state:
|
|
|
47
47
|
|
|
48
48
|
A model-backed task must not launch without an event bus. If no terminal event arrives before the task timeout, Relay records an explicit timeout failure. A Codex process exit or ChatGPT submission receipt alone never changes the task to a terminal state.
|
|
49
49
|
|
|
50
|
+
For Codex, an unexpected process exit after the worker's `task.started` is given only a short bounded terminal grace window. If no exact correlated worker terminal event arrives in that window, Relay records `task.failed` with `worker_exit_without_terminal_event` as an explicit lifecycle-contract failure. This watchdog failure is still distinct from `task.process.exited`; it never treats process exit as task success and never duplicates an already-applied terminal event. An exit before `task.started` is likewise recorded as `worker_exit_before_start`.
|
|
51
|
+
|
|
50
52
|
## Managed GitHub jobs
|
|
51
53
|
|
|
52
54
|
For repository work, create/adopt/repair the PR-backed job through the Agents Relay CLI. Do not create the managed PR through a parallel raw GitHub path.
|
|
@@ -17,6 +17,7 @@ Inspect the durable objective and task tree, then decide whether the objective i
|
|
|
17
17
|
- `{"kind":"file","path":"..."}`.
|
|
18
18
|
- Include routing metadata for every model-backed child task.
|
|
19
19
|
- Do not call an execution agent "codex"; agent identity describes responsibility, while the adapter describes runtime.
|
|
20
|
+
- If a child task includes `priority`, emit the canonical string `P0`, `P1`, `P2`, or `P3` exactly. Do not emit numeric priorities, lowercase aliases, or labels such as `high`/`medium`/`low`.
|
|
20
21
|
- Return no prose outside the JSON result.
|
|
21
22
|
|
|
22
23
|
## Output
|