agents-relay 2.0.130 → 2.0.131
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/reconciler.js
CHANGED
|
@@ -21,6 +21,7 @@ import { executionEventSource, withExecutionBoundary } from './reconciler-runtim
|
|
|
21
21
|
export { executionEventSource } from './reconciler-runtime.js';
|
|
22
22
|
import { CallbackWorkflowEffectHandler } from './workflow/effects.js';
|
|
23
23
|
import { exhaustedTroubleshooterRecoveries, pendingTroubleshooterTasks, pendingTroubleshooterReopens, refreshTroubleshooterInput } from './troubleshooter.js';
|
|
24
|
+
import { fallbackFailedTroubleshooters, escalateExhaustedTroubleshooters } from './troubleshooter-fallback.js';
|
|
24
25
|
import { taskUsesCanonicalProjectRun } from './workspace.js';
|
|
25
26
|
import { cleanupMergedJobWorktree } from './job-worktree-cleanup.js';
|
|
26
27
|
import { jobHasActiveExecution } from './job-execution-slot.js';
|
|
@@ -145,6 +146,10 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
145
146
|
await this.store.saveTask(recovery);
|
|
146
147
|
await this.emit(eventFor(job.id, recovery.id, original.id, 'troubleshooter.retry_queued', 'queued', `Retrying failed Troubleshooter ${recovery.id} (attempt ${recovery.attempt + 1}/${recovery.maxAttempts})`, 'orchestrator', { originalTaskId: original.id, attempt: recovery.attempt + 1, maxAttempts: recovery.maxAttempts }));
|
|
147
148
|
}
|
|
149
|
+
for (const id of await fallbackFailedTroubleshooters(this.store, jobId))
|
|
150
|
+
await this.emit(eventFor(job.id, id, null, 'troubleshooter.retry_queued', 'queued', `Alternate model fallback scheduled for ${id}`, 'orchestrator'));
|
|
151
|
+
for (const id of await escalateExhaustedTroubleshooters(this.store, jobId))
|
|
152
|
+
await this.emit(eventFor(job.id, id, null, 'task.blocked', 'blocked', `Fallback exhausted: human action required for ${id}`, 'orchestrator'));
|
|
148
153
|
const exhaustedJob = await this.store.load(jobId);
|
|
149
154
|
for (const exhausted of exhaustedTroubleshooterRecoveries(exhaustedJob)) {
|
|
150
155
|
const at = new Date().toISOString();
|
package/dist/task-recovery.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function requestTroubleshooterEscalation(store, jobId, recoveryTask
|
|
|
36
36
|
result = { task: structuredClone(recovery), escalation: structuredClone(recovery.escalation), duplicate: true };
|
|
37
37
|
return job;
|
|
38
38
|
}
|
|
39
|
-
if (!['QUEUED', 'RUNNING'].includes(recovery.state))
|
|
39
|
+
if (!['QUEUED', 'RUNNING', 'FAILED'].includes(recovery.state))
|
|
40
40
|
throw new Error(`Troubleshooter ${recoveryTaskId} cannot escalate from ${recovery.state}`);
|
|
41
41
|
const proposal = { path: 'escalation_gate', targetTaskId: original.id, rootCause: request.context.trim(), solution: request.requiredAction.trim() };
|
|
42
42
|
const decision = dispatchTaskResolutionSync(original, proposal, 'troubleshooter', {
|
|
@@ -44,7 +44,7 @@ export async function requestTroubleshooterEscalation(store, jobId, recoveryTask
|
|
|
44
44
|
});
|
|
45
45
|
if (!decision.accepted)
|
|
46
46
|
throw new Error(`Task resolution graph rejected escalation for ${original.id}`);
|
|
47
|
-
if (!tryTransitionTaskEvent(recovery, 'task.blocked').accepted)
|
|
47
|
+
if (!tryTransitionTaskEvent(recovery, 'task.blocked', { recoverableExecution: true }).accepted)
|
|
48
48
|
throw new Error(`Troubleshooter ${recoveryTaskId} cannot be blocked`);
|
|
49
49
|
const escalation = {
|
|
50
50
|
id: randomUUID(), originalTaskId: original.id, originalExecutionId: original.executionId ?? undefined,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { tryTransitionTaskEvent } from './scheduler.js';
|
|
2
|
+
import { requestTroubleshooterEscalation } from './task-recovery.js';
|
|
3
|
+
const MARKER = '[Relay execution fallback attempted]';
|
|
4
|
+
const RUNTIME_FAILURE = /worker_process_failed|worker_start_timeout|terminal_event_timeout|model.*not supported|unsupported.model|delivery|process.exited|provider/i;
|
|
5
|
+
/** One bounded Codex-config-default retry after normal recovery attempts. */
|
|
6
|
+
export async function fallbackFailedTroubleshooters(store, jobId) {
|
|
7
|
+
const job = await store.load(jobId);
|
|
8
|
+
const queued = [];
|
|
9
|
+
for (const recovery of job.tasks.filter(task => task.agentName === 'troubleshooter' && task.state === 'FAILED' && task.attempt >= task.maxAttempts && !task.input.includes(MARKER))) {
|
|
10
|
+
const original = job.tasks.find(task => task.id === recovery.parentTaskId);
|
|
11
|
+
if (!original || original.state !== 'FAILED' || !RUNTIME_FAILURE.test(`${recovery.failureReason ?? ''} ${recovery.error ?? ''}`))
|
|
12
|
+
continue;
|
|
13
|
+
const update = (task) => {
|
|
14
|
+
if (task.state !== 'FAILED' || task.input.includes(MARKER))
|
|
15
|
+
return false;
|
|
16
|
+
if (!tryTransitionTaskEvent(task, 'retry').accepted)
|
|
17
|
+
throw new Error(`Fallback retry rejected for ${task.id}`);
|
|
18
|
+
task.maxAttempts = Math.max(task.maxAttempts, task.attempt + 1);
|
|
19
|
+
task.input += `\n\n${MARKER} Retry diagnosis with Codex config default model; report valid resolution or explicit blocker.`;
|
|
20
|
+
task.routing = { provider: '', model: '', decidedBy: 'troubleshooter-fallback:codex-config-default', decidedAt: new Date().toISOString() };
|
|
21
|
+
task.workerSelection = undefined;
|
|
22
|
+
task.leaseOwner = null;
|
|
23
|
+
task.leaseExpiresAt = null;
|
|
24
|
+
task.executionId = null;
|
|
25
|
+
return true;
|
|
26
|
+
};
|
|
27
|
+
if (store.mutateJob) {
|
|
28
|
+
await store.mutateJob(jobId, current => {
|
|
29
|
+
const task = current.tasks.find(item => item.id === recovery.id);
|
|
30
|
+
if (task && update(task))
|
|
31
|
+
queued.push(task.id);
|
|
32
|
+
return current;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const task = (await store.load(jobId)).tasks.find(item => item.id === recovery.id);
|
|
37
|
+
if (task && update(task)) {
|
|
38
|
+
await store.saveTask(task);
|
|
39
|
+
queued.push(task.id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return queued;
|
|
44
|
+
}
|
|
45
|
+
/** The Core, not the crashed Agent, creates a durable human action gate. */
|
|
46
|
+
export async function escalateExhaustedTroubleshooters(store, jobId) {
|
|
47
|
+
const job = await store.load(jobId);
|
|
48
|
+
const escalated = [];
|
|
49
|
+
for (const recovery of job.tasks.filter(task => task.agentName === 'troubleshooter' && task.state === 'FAILED' && task.attempt >= task.maxAttempts && !task.escalation)) {
|
|
50
|
+
const original = job.tasks.find(task => task.id === recovery.parentTaskId);
|
|
51
|
+
if (!original || original.state !== 'FAILED')
|
|
52
|
+
continue;
|
|
53
|
+
const context = `Automatic Troubleshooter ${recovery.id} could not produce a verified resolution after ${recovery.attempt} attempts. Last failure: ${recovery.error ?? recovery.failureReason ?? 'unknown'}. Original Task ${original.id}: ${original.error ?? original.failureReason ?? 'failed'}.`;
|
|
54
|
+
const result = await requestTroubleshooterEscalation(store, jobId, recovery.id, {
|
|
55
|
+
requiredAction: `Inspect Task ${original.id} and Troubleshooter ${recovery.id}; correct provider/runtime or supply a concrete recovery decision and supporting evidence.`,
|
|
56
|
+
resolutionCondition: `User supplies a corrective action or verified recovery instructions for Task ${original.id}.`, context,
|
|
57
|
+
});
|
|
58
|
+
if (!result.duplicate)
|
|
59
|
+
escalated.push(recovery.id);
|
|
60
|
+
}
|
|
61
|
+
return escalated;
|
|
62
|
+
}
|