agents-relay 2.0.156 → 2.0.158
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/blockers.js +6 -27
- package/dist/chatgpt-platform-fallback.js +1 -1
- package/dist/cli-help.js +5 -6
- package/dist/cli-plan-clean-history.js +16 -0
- package/dist/cli-unblock.js +3 -14
- package/dist/cli-worker-task.js +11 -0
- package/dist/cli.js +27 -16
- package/dist/dashboard-actions.js +9 -19
- package/dist/dashboard-data.js +45 -0
- package/dist/dashboard-html.js +1 -1
- package/dist/dashboard.js +2 -52
- package/dist/host-invocation-store.js +2 -2
- package/dist/interactive-invocation.js +1 -1
- package/dist/interactive-resume.js +5 -5
- package/dist/legacy-blocked-task-migration.js +14 -0
- package/dist/legacy-orchestrator-repair.js +6 -6
- package/dist/manual-plan.js +19 -1
- package/dist/planner-recovery.js +1 -1
- package/dist/planner-runtime.js +9 -1
- package/dist/planner.js +18 -23
- package/dist/projection-outbox.js +1 -1
- package/dist/reconciler-review.js +4 -3
- package/dist/reconciler-runtime.js +15 -19
- package/dist/reconciler.js +3 -3
- package/dist/relay-store.js +2 -0
- package/dist/relayd-control.js +14 -7
- package/dist/single-job-task-control.js +1 -1
- package/dist/task-actions.js +28 -3
- package/dist/task-recovery.js +1 -1
- package/dist/troubleshooter.js +1 -1
- package/dist/types.js +12 -6
- package/dist/unblock.js +5 -44
- package/dist/workflow/authoritative-unblock-v2.js +6 -7
- package/dist/workflow/contracts.js +3 -0
- package/dist/workflow/definitions-v2.js +3 -2
- package/dist/workflow/lifecycle.js +5 -5
- package/dist/workflow/orchestrator-rescue.js +13 -12
- package/dist/workflow/runtime.js +13 -2
- package/dist/workflow/task-resolution.js +52 -15
- package/dist/workflow/troubleshooter-v2.js +5 -3
- package/package.json +3 -3
- package/skills/agents-relay/agents/planner.agent.md +2 -2
- package/skills/agents-relay/agents/troubleshooter.agent.md +5 -3
- package/skills/agents-relay/contracts/task-job-contract.md +18 -6
- package/workflow/authoritative-unblock.mmd +6 -9
- package/workflow/blocker-resolution.mmd +6 -16
- package/workflow/orchestrator-rescue.mmd +3 -3
- package/workflow/task-lifecycle.mmd +45 -59
- package/workflow/task-resolution.mmd +24 -3
- package/workflow/troubleshooter.mmd +20 -1
|
@@ -12,6 +12,7 @@ export const WORKFLOW_EVENT_NAMES = [
|
|
|
12
12
|
'hold',
|
|
13
13
|
'lease.expired',
|
|
14
14
|
'implementation.completed',
|
|
15
|
+
'interactive.waiting',
|
|
15
16
|
'local.completed',
|
|
16
17
|
'local.merge.requested',
|
|
17
18
|
'effect.deliverLocalMerge.failed',
|
|
@@ -38,6 +39,7 @@ export const WORKFLOW_EVENT_NAMES = [
|
|
|
38
39
|
'task.progress',
|
|
39
40
|
'task.started',
|
|
40
41
|
'task.unblocked',
|
|
42
|
+
'interactive.waiting',
|
|
41
43
|
'watchdog.failed',
|
|
42
44
|
'worker.delivery_failed',
|
|
43
45
|
'worker.start_ack_failed',
|
|
@@ -70,6 +72,7 @@ export const WORKFLOW_MUTATION_NAMES = [
|
|
|
70
72
|
'mutation.clearExecution',
|
|
71
73
|
'mutation.markLeaseExpired',
|
|
72
74
|
'mutation.markStartupOrphanedExhausted',
|
|
75
|
+
'mutation.markUnknownOutcome',
|
|
73
76
|
'mutation.markPlannerUnavailable',
|
|
74
77
|
'mutation.preparePlatformFallback',
|
|
75
78
|
'mutation.quarantinePlatformFailure',
|
|
@@ -5,7 +5,7 @@ import { WORKFLOW_EFFECT_NAMES, WORKFLOW_EVENT_NAMES, WORKFLOW_MUTATION_NAMES, }
|
|
|
5
5
|
const TASK_STATES = ['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED'];
|
|
6
6
|
const JOB_STATES = ['DRAFT', 'ACTIVE', 'COMPLETED', 'CANCELLED'];
|
|
7
7
|
const conditionRegistry = new ConditionRegistry([
|
|
8
|
-
...new Set(['dependenciesSatisfied', 'dependenciesUnsatisfied', 'runnable', 'dispatchable', 'recoverableExecution', 'troubleshooterRecovery', '
|
|
8
|
+
...new Set(['dependenciesSatisfied', 'dependenciesUnsatisfied', 'runnable', 'dispatchable', 'recoverableExecution', 'troubleshooterRecovery', 'troubleshooterTask', 'awaitingInteractiveDecision', 'retryAvailable', 'autonomousJob', 'notMerged', 'autoMerge', 'prOpen', 'reviewDisabled', 'implementationComplete', 'headAvailable', 'mergeCapable', 'localCompletionAllowed', 'localMergeReady'])
|
|
9
9
|
].map(booleanCondition));
|
|
10
10
|
const boolGuard = (name) => conditionRegistry.guard(name);
|
|
11
11
|
export const lifecycleConditions = conditionRegistry;
|
|
@@ -31,9 +31,10 @@ export const taskLifecycleV2 = load('task-lifecycle', TASK_STATES, {
|
|
|
31
31
|
dispatchable: boolGuard('dispatchable'),
|
|
32
32
|
recoverableExecution: boolGuard('recoverableExecution'),
|
|
33
33
|
troubleshooterRecovery: boolGuard('troubleshooterRecovery'),
|
|
34
|
+
troubleshooterTask: boolGuard('troubleshooterTask'),
|
|
35
|
+
awaitingInteractiveDecision: facts => facts.durableFacts.awaitingInteractiveDecision === true,
|
|
34
36
|
retryAvailable: boolGuard('retryAvailable'),
|
|
35
37
|
autonomousJob: boolGuard('autonomousJob'),
|
|
36
|
-
platformFallbackSafe: boolGuard('platformFallbackSafe'),
|
|
37
38
|
});
|
|
38
39
|
export const jobLifecycleV2 = load('job-lifecycle', JOB_STATES, {
|
|
39
40
|
notMerged: boolGuard('notMerged'),
|
|
@@ -26,7 +26,7 @@ function v2WorkflowDecision(runtime, from, event, facts = {}) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
export function decideTaskLifecycle(task, event, facts = {}) {
|
|
29
|
-
return v2WorkflowDecision(taskLifecycleV2, task.state, event, facts);
|
|
29
|
+
return v2WorkflowDecision(taskLifecycleV2, task.state, event, { ...facts, troubleshooterTask: task.agentName === 'troubleshooter' });
|
|
30
30
|
}
|
|
31
31
|
export function decideJobLifecycle(job, event, facts = {}) {
|
|
32
32
|
return v2WorkflowDecision(jobLifecycleV2, job.state, event, facts);
|
|
@@ -36,9 +36,9 @@ export function dispatchTaskLifecycleSync(task, event, facts = {}) {
|
|
|
36
36
|
const result = dispatchWorkflowGraphSync(taskLifecycleV2, {
|
|
37
37
|
initialState: task.state,
|
|
38
38
|
input: { transport: 'event', id: event, payload: {} },
|
|
39
|
-
durableFacts: facts,
|
|
39
|
+
durableFacts: { ...facts, troubleshooterTask: task.agentName === 'troubleshooter' },
|
|
40
40
|
graphName: 'task-lifecycle',
|
|
41
|
-
persistTransition: (_source, target) => applyTaskGraphTarget(task, target),
|
|
41
|
+
persistTransition: (_source, target) => applyTaskGraphTarget(task, target, new Date().toISOString(), event === 'task.unblocked' && task.agentName === 'troubleshooter'),
|
|
42
42
|
executeAction: actionId => { actions.push(actionId); if (actionId.startsWith('mutation.'))
|
|
43
43
|
applyTaskMutation(task, actionId); return {}; },
|
|
44
44
|
});
|
|
@@ -49,9 +49,9 @@ export function dispatchTaskLifecycleSync(task, event, facts = {}) {
|
|
|
49
49
|
export async function dispatchTaskLifecycle(task, event, facts = {}, executeEffect, persist) {
|
|
50
50
|
const actions = [];
|
|
51
51
|
const result = await dispatchWorkflowGraph(taskLifecycleV2, {
|
|
52
|
-
initialState: task.state, input: { transport: 'event', id: event, payload: {} }, durableFacts: facts,
|
|
52
|
+
initialState: task.state, input: { transport: 'event', id: event, payload: {} }, durableFacts: { ...facts, troubleshooterTask: task.agentName === 'troubleshooter' },
|
|
53
53
|
graphName: 'task-lifecycle',
|
|
54
|
-
persistTransition: async (_source, target) => { applyTaskGraphTarget(task, target); await persist?.(); },
|
|
54
|
+
persistTransition: async (_source, target) => { applyTaskGraphTarget(task, target, new Date().toISOString(), event === 'task.unblocked' && task.agentName === 'troubleshooter'); await persist?.(); },
|
|
55
55
|
executeAction: async (actionId) => {
|
|
56
56
|
actions.push(actionId);
|
|
57
57
|
if (actionId.startsWith('mutation.'))
|
|
@@ -7,7 +7,7 @@ export const STALE_ORCHESTRATOR_TASK_MS = 30 * 60 * 1000;
|
|
|
7
7
|
const source = readFileSync(new URL('../../workflow/orchestrator-rescue.mmd', import.meta.url), 'utf8');
|
|
8
8
|
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
9
9
|
export const orchestratorRescueV2 = new GraphRuntime(source, {
|
|
10
|
-
states: new Set(['idle', 'check', 'external_completed', '
|
|
10
|
+
states: new Set(['idle', 'check', 'external_completed', 'failed', 'skip', 'rescued']),
|
|
11
11
|
guards: {
|
|
12
12
|
legacyActive: boolGuard('legacyActive'),
|
|
13
13
|
verifiedExternalCompletion: boolGuard('verifiedExternalCompletion'),
|
|
@@ -16,7 +16,7 @@ export const orchestratorRescueV2 = new GraphRuntime(source, {
|
|
|
16
16
|
},
|
|
17
17
|
actions: {
|
|
18
18
|
'task.applyExternalCompletion': { id: 'task.applyExternalCompletion' },
|
|
19
|
-
'task.
|
|
19
|
+
'task.failLegacyOrchestrator': { id: 'task.failLegacyOrchestrator' },
|
|
20
20
|
'task.rescueOrchestrator': { id: 'task.rescueOrchestrator' },
|
|
21
21
|
},
|
|
22
22
|
events: { 'orchestrator.rescue.reconcile': { id: 'orchestrator.rescue.reconcile' } },
|
|
@@ -46,9 +46,9 @@ export function prepareOrchestratorRepair(task, now) {
|
|
|
46
46
|
const decision = decideOrchestratorRescue(task, now);
|
|
47
47
|
const externallyCompleted = decision.accepted
|
|
48
48
|
&& decision.actions.some(action => action.id === 'task.applyExternalCompletion');
|
|
49
|
-
const
|
|
50
|
-
&& decision.actions.some(action => action.id === 'task.
|
|
51
|
-
const rescued = externallyCompleted ||
|
|
49
|
+
const failed = decision.accepted
|
|
50
|
+
&& decision.actions.some(action => action.id === 'task.failLegacyOrchestrator');
|
|
51
|
+
const rescued = externallyCompleted || failed
|
|
52
52
|
|| (decision.accepted && decision.actions.some(action => action.id === 'task.rescueOrchestrator'));
|
|
53
53
|
const ageMs = now - Date.parse(task.createdAt);
|
|
54
54
|
if (decision.accepted && externallyCompleted) {
|
|
@@ -58,19 +58,20 @@ export function prepareOrchestratorRepair(task, now) {
|
|
|
58
58
|
task.executionId = null;
|
|
59
59
|
task.threadId = null;
|
|
60
60
|
task.updatedAt = new Date(now).toISOString();
|
|
61
|
-
return { repaired: true, externallyCompleted: true,
|
|
61
|
+
return { repaired: true, externallyCompleted: true, failed: false, ageMs };
|
|
62
62
|
}
|
|
63
63
|
if (!rescued)
|
|
64
|
-
return { repaired: false, externallyCompleted: false,
|
|
65
|
-
if (
|
|
66
|
-
tryTransitionTaskEvent(task, 'task.
|
|
67
|
-
task.error = 'Legacy orchestrator execution outcome is unknown;
|
|
64
|
+
return { repaired: false, externallyCompleted: false, failed: false, ageMs };
|
|
65
|
+
if (failed) {
|
|
66
|
+
tryTransitionTaskEvent(task, 'task.failed');
|
|
67
|
+
task.error = 'Legacy orchestrator execution outcome is unknown; Troubleshooter diagnosis is required before retry.';
|
|
68
|
+
task.failureReason = 'worker_outcome_unknown';
|
|
68
69
|
task.leaseOwner = null;
|
|
69
70
|
task.leaseExpiresAt = null;
|
|
70
71
|
task.executionId = null;
|
|
71
72
|
task.threadId = null;
|
|
72
73
|
task.updatedAt = new Date(now).toISOString();
|
|
73
|
-
return { repaired: true, externallyCompleted: false,
|
|
74
|
+
return { repaired: true, externallyCompleted: false, failed: true, ageMs };
|
|
74
75
|
}
|
|
75
76
|
task.adapter = 'codex';
|
|
76
77
|
task.workerRequirements = { capabilities: [...task.capabilities] };
|
|
@@ -82,5 +83,5 @@ export function prepareOrchestratorRepair(task, now) {
|
|
|
82
83
|
task.threadId = null;
|
|
83
84
|
task.timeoutMs = taskOverallTimeoutMs(task.storySize ?? 'M');
|
|
84
85
|
task.updatedAt = new Date(now).toISOString();
|
|
85
|
-
return { repaired: true, externallyCompleted: false,
|
|
86
|
+
return { repaired: true, externallyCompleted: false, failed: false, ageMs };
|
|
86
87
|
}
|
package/dist/workflow/runtime.js
CHANGED
|
@@ -32,6 +32,10 @@ export function applyTaskMutation(task, mutation) {
|
|
|
32
32
|
task.error = 'Lease expired after maximum attempts';
|
|
33
33
|
if (mutation === 'mutation.markStartupOrphanedExhausted')
|
|
34
34
|
task.error = 'Orphaned after relayd restart after maximum attempts';
|
|
35
|
+
if (mutation === 'mutation.markUnknownOutcome') {
|
|
36
|
+
task.error = 'Worker execution outcome is unknown; Troubleshooter diagnosis is required before retry.';
|
|
37
|
+
task.failureReason = 'worker_outcome_unknown';
|
|
38
|
+
}
|
|
35
39
|
if (mutation === 'mutation.markPlannerUnavailable')
|
|
36
40
|
task.error = 'No objective planner configured';
|
|
37
41
|
if (mutation === 'mutation.preparePlatformFallback') {
|
|
@@ -49,10 +53,17 @@ export function applyTaskMutation(task, mutation) {
|
|
|
49
53
|
task.leaseOwner = null;
|
|
50
54
|
task.leaseExpiresAt = null;
|
|
51
55
|
task.executionId = null;
|
|
52
|
-
task.failureReason = task.platformFailure?.
|
|
56
|
+
task.failureReason = ['outcome_unknown', 'start_ack_timeout'].includes(task.platformFailure?.classification ?? '')
|
|
57
|
+
? 'worker_outcome_unknown' : 'worker_platform_failure';
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
|
-
export function applyTaskGraphTarget(task, target, timestamp = new Date().toISOString()) {
|
|
60
|
+
export function applyTaskGraphTarget(task, target, timestamp = new Date().toISOString(), authoritativeUnblock = false) {
|
|
61
|
+
if (target === 'BLOCKED' && task.agentName !== 'troubleshooter')
|
|
62
|
+
throw new Error(`Only a Troubleshooter Task may become BLOCKED (${task.id})`);
|
|
63
|
+
if (task.state === 'BLOCKED' && target === 'QUEUED' && (!authoritativeUnblock || task.agentName !== 'troubleshooter'))
|
|
64
|
+
throw new Error(`Blocked Task ${task.id} requires an authoritative Troubleshooter unblock before transition`);
|
|
65
|
+
if (task.state === 'BLOCKED' && target !== 'QUEUED' && target !== 'CANCELLED' && target !== 'BLOCKED')
|
|
66
|
+
throw new Error(`Blocked Task ${task.id} requires an authoritative Troubleshooter unblock before transition`);
|
|
56
67
|
const previous = Date.parse(task.updatedAt), candidate = Date.parse(timestamp);
|
|
57
68
|
task.state = target;
|
|
58
69
|
task.updatedAt = Number.isFinite(previous)
|
|
@@ -7,23 +7,53 @@ export const TASK_RESOLUTION_PATHS = [
|
|
|
7
7
|
const source = readFileSync(new URL('../../workflow/task-resolution.mmd', import.meta.url), 'utf8');
|
|
8
8
|
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
9
9
|
export const taskResolutionV2 = new GraphRuntime(source, {
|
|
10
|
-
states: new Set(['unresolved', 'check_owner', 'check_path', 'resolved']),
|
|
10
|
+
states: new Set(['unresolved', 'check_owner', 'check_path', 'check_fresh_execution', 'check_classification', 'classificationKind', 'resolved', 'rejected']),
|
|
11
11
|
guards: {
|
|
12
12
|
recoveryOwnerAvailable: boolGuard('recoveryOwnerAvailable'), plannerOwnerAvailable: boolGuard('plannerOwnerAvailable'),
|
|
13
13
|
exactOriginalRetry: boolGuard('exactOriginalRetry'), sameJobDependency: boolGuard('sameJobDependency'),
|
|
14
14
|
blockerJobDependency: boolGuard('blockerJobDependency'), escalationGate: boolGuard('escalationGate'), genuinelyObsolete: boolGuard('genuinelyObsolete'),
|
|
15
|
+
freshRecoveryExecution: boolGuard('freshRecoveryExecution'),
|
|
16
|
+
classifiableFailure: boolGuard('classifiableFailure'),
|
|
17
|
+
classificationSuperseded: boolGuard('classificationSuperseded'),
|
|
18
|
+
supersededEvidenceComplete: boolGuard('supersededEvidenceComplete'),
|
|
19
|
+
invalidEvidenceComplete: boolGuard('invalidEvidenceComplete'),
|
|
15
20
|
},
|
|
16
|
-
actions: Object.fromEntries(['resolution.activateRecovery', 'resolution.attachSameJobDependency', 'resolution.registerBlockerDependency', 'resolution.createEscalationGate', 'resolution.cancelSupersede'].map(id => [id, { id }])),
|
|
17
|
-
events: { 'resolution.propose': { id: 'resolution.propose' } },
|
|
21
|
+
actions: Object.fromEntries(['resolution.activateRecovery', 'resolution.attachSameJobDependency', 'resolution.registerBlockerDependency', 'resolution.createEscalationGate', 'resolution.cancelSupersede', 'resolution.classifySuperseded', 'resolution.classifyInvalid'].map(id => [id, { id }])),
|
|
22
|
+
events: { 'resolution.propose': { id: 'resolution.propose' }, 'resolution.classify': { id: 'resolution.classify' } },
|
|
18
23
|
});
|
|
24
|
+
export function classifyTaskResolution(task, classification, now = new Date().toISOString()) {
|
|
25
|
+
const complete = Boolean(classification.targetTaskId.trim() && classification.evidence.trim() && classification.reason.trim());
|
|
26
|
+
let applied;
|
|
27
|
+
const decision = dispatchWorkflowGraphSync(taskResolutionV2, {
|
|
28
|
+
initialState: 'unresolved', input: { transport: 'event', id: 'resolution.classify', payload: classification },
|
|
29
|
+
durableFacts: {
|
|
30
|
+
classifiableFailure: task.kind !== 'planner' && task.state === 'FAILED' && task.failureReason !== 'worker_outcome_unknown' && !task.executionId && !task.leaseOwner && !task.leaseExpiresAt && !task.resolutionClassification && !['active', 'waiting'].includes(task.resolution?.state ?? ''),
|
|
31
|
+
classificationSuperseded: classification.kind === 'superseded',
|
|
32
|
+
supersededEvidenceComplete: classification.kind === 'superseded' && complete,
|
|
33
|
+
invalidEvidenceComplete: classification.kind === 'invalid' && complete,
|
|
34
|
+
}, graphName: 'task-resolution', isTransientState: state => state === 'check_classification' || state === 'classificationKind', maxSteps: 6,
|
|
35
|
+
executeAction: actionId => {
|
|
36
|
+
const expectedAction = classification.kind === 'superseded' ? 'resolution.classifySuperseded' : 'resolution.classifyInvalid';
|
|
37
|
+
if (actionId !== expectedAction)
|
|
38
|
+
throw new Error(`Unexpected Task Resolution action ${actionId}`);
|
|
39
|
+
applied = { ...classification, updatedAt: now };
|
|
40
|
+
task.resolutionClassification = applied;
|
|
41
|
+
task.updatedAt = now;
|
|
42
|
+
return {};
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
if (!decision.accepted || decision.target !== 'resolved' || !applied)
|
|
46
|
+
throw new Error(`Task resolution graph rejected ${classification.kind} classification for ${task.id}`);
|
|
47
|
+
return applied;
|
|
48
|
+
}
|
|
19
49
|
export function unresolvedTask(task) { return task.kind !== 'planner' && !['SUCCEEDED', 'CANCELLED'].includes(task.state); }
|
|
20
50
|
export function validateResolutionProposal(job, original, proposal) {
|
|
21
51
|
if (!TASK_RESOLUTION_PATHS.includes(proposal.path))
|
|
22
52
|
throw new Error(`Unknown task resolution path ${proposal.path}`);
|
|
23
53
|
if (proposal.path === 'exact_original_retry' && original.state !== 'FAILED')
|
|
24
54
|
throw new Error('Exact-original retry requires a FAILED original Task');
|
|
25
|
-
if (proposal.path === 'same_job_dependency' && (!proposal.targetTaskId || !job.tasks.some(task => task.id === proposal.targetTaskId && task.jobId === job.id)))
|
|
26
|
-
throw new Error('Same-Job dependency resolution requires an existing target Task in the same Job');
|
|
55
|
+
if (proposal.path === 'same_job_dependency' && (!proposal.targetTaskId || proposal.targetTaskId === original.id || !job.tasks.some(task => task.id === proposal.targetTaskId && task.jobId === job.id && !['SUCCEEDED', 'CANCELLED'].includes(task.state))))
|
|
56
|
+
throw new Error('Same-Job dependency resolution requires an existing unresolved target Task in the same Job');
|
|
27
57
|
if (proposal.path === 'blocker_job_dependency' && (!proposal.targetJobId || !proposal.mode))
|
|
28
58
|
throw new Error('Blocker-Job dependency resolution requires targetJobId and merge or verified mode');
|
|
29
59
|
if (proposal.path === 'escalation_gate' && (!proposal.rootCause || !proposal.solution))
|
|
@@ -32,10 +62,15 @@ export function validateResolutionProposal(job, original, proposal) {
|
|
|
32
62
|
throw new Error('Cancellation/supersession requires an explicit obsolescence rootCause');
|
|
33
63
|
}
|
|
34
64
|
export function resolutionFacts(task, proposal, owner) {
|
|
35
|
-
|
|
65
|
+
const freshRecoveryExecution = owner !== 'troubleshooter' || proposal.path !== 'exact_original_retry' || (Boolean(proposal.recoveryExecutionId)
|
|
66
|
+
&& proposal.recoveryExecutionId !== task.resolution?.recoveryExecutionId);
|
|
67
|
+
return { recoveryOwnerAvailable: owner === 'troubleshooter', plannerOwnerAvailable: owner !== null && owner !== 'troubleshooter', exactOriginalRetry: proposal.path === 'exact_original_retry' && task.state === 'FAILED', sameJobDependency: proposal.path === 'same_job_dependency' && owner !== null && proposal.targetTaskId !== task.id, blockerJobDependency: proposal.path === 'blocker_job_dependency' && owner !== null && Boolean(proposal.targetJobId && proposal.mode), escalationGate: proposal.path === 'escalation_gate' && owner === 'troubleshooter' && Boolean(proposal.rootCause && proposal.solution), genuinelyObsolete: proposal.path === 'cancel_supersede' && Boolean(proposal.rootCause), freshRecoveryExecution, unresolved: unresolvedTask(task) };
|
|
36
68
|
}
|
|
37
69
|
export function decideTaskResolution(task, proposal, owner) {
|
|
38
|
-
return dispatchWorkflowGraphSync(taskResolutionV2, { initialState: 'unresolved', input: { transport: 'event', id: 'resolution.propose', payload: proposal }, durableFacts: resolutionFacts(task, proposal, owner), graphName: 'task-resolution', isTransientState: state =>
|
|
70
|
+
return checkedResolutionDecision(dispatchWorkflowGraphSync(taskResolutionV2, { initialState: 'unresolved', input: { transport: 'event', id: 'resolution.propose', payload: proposal }, durableFacts: resolutionFacts(task, proposal, owner), graphName: 'task-resolution', isTransientState: state => ['check_owner', 'check_path', 'check_fresh_execution'].includes(state), maxSteps: 8 }));
|
|
71
|
+
}
|
|
72
|
+
function checkedResolutionDecision(decision) {
|
|
73
|
+
return decision.accepted && decision.target === 'rejected' ? { ...decision, accepted: false, reason: 'no_transition' } : decision;
|
|
39
74
|
}
|
|
40
75
|
export function dispatchTaskResolutionSync(task, proposal, owner, effects) {
|
|
41
76
|
const handlers = {
|
|
@@ -45,13 +80,13 @@ export function dispatchTaskResolutionSync(task, proposal, owner, effects) {
|
|
|
45
80
|
'resolution.createEscalationGate': effects.createEscalationGate,
|
|
46
81
|
'resolution.cancelSupersede': effects.cancelSupersede,
|
|
47
82
|
};
|
|
48
|
-
return dispatchWorkflowGraphSync(taskResolutionV2, {
|
|
83
|
+
return checkedResolutionDecision(dispatchWorkflowGraphSync(taskResolutionV2, {
|
|
49
84
|
initialState: 'unresolved', input: { transport: 'event', id: 'resolution.propose', payload: proposal },
|
|
50
85
|
durableFacts: resolutionFacts(task, proposal, owner), graphName: 'task-resolution',
|
|
51
|
-
isTransientState: state =>
|
|
86
|
+
isTransientState: state => ['check_owner', 'check_path', 'check_fresh_execution'].includes(state), maxSteps: 8,
|
|
52
87
|
executeAction: actionId => { const handler = handlers[actionId]; if (!handler)
|
|
53
88
|
throw new Error(`Task resolution effect ${actionId} is unavailable`); handler(); return {}; },
|
|
54
|
-
});
|
|
89
|
+
}));
|
|
55
90
|
}
|
|
56
91
|
export async function dispatchTaskResolution(task, proposal, owner, effects) {
|
|
57
92
|
const handlers = {
|
|
@@ -61,13 +96,13 @@ export async function dispatchTaskResolution(task, proposal, owner, effects) {
|
|
|
61
96
|
'resolution.createEscalationGate': effects.createEscalationGate,
|
|
62
97
|
'resolution.cancelSupersede': effects.cancelSupersede,
|
|
63
98
|
};
|
|
64
|
-
return dispatchWorkflowGraph(taskResolutionV2, {
|
|
99
|
+
return checkedResolutionDecision(await dispatchWorkflowGraph(taskResolutionV2, {
|
|
65
100
|
initialState: 'unresolved',
|
|
66
101
|
input: { transport: 'event', id: 'resolution.propose', payload: proposal },
|
|
67
102
|
durableFacts: resolutionFacts(task, proposal, owner),
|
|
68
103
|
graphName: 'task-resolution',
|
|
69
|
-
isTransientState: state =>
|
|
70
|
-
maxSteps:
|
|
104
|
+
isTransientState: state => ['check_owner', 'check_path', 'check_fresh_execution'].includes(state),
|
|
105
|
+
maxSteps: 8,
|
|
71
106
|
executeAction: async (actionId) => {
|
|
72
107
|
const handler = handlers[actionId];
|
|
73
108
|
if (!handler)
|
|
@@ -75,7 +110,7 @@ export async function dispatchTaskResolution(task, proposal, owner, effects) {
|
|
|
75
110
|
await handler();
|
|
76
111
|
return {};
|
|
77
112
|
},
|
|
78
|
-
});
|
|
113
|
+
}));
|
|
79
114
|
}
|
|
80
115
|
export function materializeResolution(task, proposal, owner, now = new Date().toISOString()) {
|
|
81
116
|
return { ...proposal, state: proposal.path === 'escalation_gate' || proposal.path === 'blocker_job_dependency' ? 'waiting' : 'active', owner, updatedAt: now };
|
|
@@ -100,5 +135,7 @@ export function attachResolutionOwnership(job, child, proposal, owner) {
|
|
|
100
135
|
return original;
|
|
101
136
|
}
|
|
102
137
|
export function claimExactOriginalRecovery(original, recovery) {
|
|
103
|
-
|
|
138
|
+
if (!recovery.executionId)
|
|
139
|
+
throw new Error(`Troubleshooter ${recovery.id} has no current execution for a fresh resolution`);
|
|
140
|
+
original.resolution = materializeResolution(original, { path: 'exact_original_retry', targetTaskId: original.id, recoveryExecutionId: recovery.executionId, rootCause: original.failureReason ?? original.error ?? 'durable failure', solution: `Troubleshooter ${recovery.id} diagnosis and exact-original recovery` }, 'troubleshooter');
|
|
104
141
|
}
|
|
@@ -4,7 +4,7 @@ import { dispatchWorkflowGraphSync } from './dispatcher.js';
|
|
|
4
4
|
const source = readFileSync(new URL('../../workflow/troubleshooter.mmd', import.meta.url), 'utf8');
|
|
5
5
|
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
6
6
|
export const troubleshooterV2 = new GraphRuntime(source, {
|
|
7
|
-
states: new Set(['idle', 'check_ordinary', 'check_run', 'check_needs', 'check_id', 'check_barrier', 'skip', 'create']),
|
|
7
|
+
states: new Set(['idle', 'check_ordinary', 'check_run', 'check_needs', 'check_id', 'check_barrier', 'skip', 'create', 'resumed_after_authoritative_unblock', 'fresh_resolution_proposed', 'remain_failed_without_original_mutation']),
|
|
8
8
|
guards: {
|
|
9
9
|
failedWork: boolGuard('failedWork'),
|
|
10
10
|
ordinaryWork: boolGuard('ordinaryWork'),
|
|
@@ -12,9 +12,11 @@ export const troubleshooterV2 = new GraphRuntime(source, {
|
|
|
12
12
|
needsRecovery: boolGuard('needsRecovery'),
|
|
13
13
|
idUnused: boolGuard('idUnused'),
|
|
14
14
|
noRecoveryBarrier: boolGuard('noRecoveryBarrier'),
|
|
15
|
+
freshConditionVerification: boolGuard('freshConditionVerification'),
|
|
16
|
+
resolutionForCurrentExecution: boolGuard('resolutionForCurrentExecution'),
|
|
15
17
|
},
|
|
16
|
-
actions: { 'troubleshooter.create': { id: 'troubleshooter.create' } },
|
|
17
|
-
events: { 'troubleshooter.reconcile': { id: 'troubleshooter.reconcile' } },
|
|
18
|
+
actions: { 'troubleshooter.create': { id: 'troubleshooter.create' }, 'troubleshooter.proposeFreshResolution': { id: 'troubleshooter.proposeFreshResolution' }, 'troubleshooter.applyAuthorizedResolution': { id: 'troubleshooter.applyAuthorizedResolution' } },
|
|
19
|
+
events: { 'troubleshooter.reconcile': { id: 'troubleshooter.reconcile' }, 'troubleshooter.resumed': { id: 'troubleshooter.resumed' }, 'task.started': { id: 'task.started' }, 'resolution.propose': { id: 'resolution.propose' } },
|
|
18
20
|
});
|
|
19
21
|
export function dispatchTroubleshooter(facts, onCreate) {
|
|
20
22
|
return dispatchWorkflowGraphSync(troubleshooterV2, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-relay",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.158",
|
|
4
4
|
"description": "Durable async agent jobs coordinated through GitHub pull requests",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"test:publish": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=1 --test-force-exit test/*.test.js",
|
|
20
20
|
"test:fast": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=2 test/workflow-lifecycle.test.js test/workflow-v2-lifecycle-parity.test.js test/workflow-effects.test.js test/relay-store.test.js test/local-authority.test.js test/local-jobid-routing.test.js test/local-sqlite-jobid.test.js test/no-github.test.js test/scheduled-jobs.test.js test/reconcile-coordinator.test.js test/service-publication.test.js test/github-webhook.test.js",
|
|
21
21
|
"test:full": "npm run build && node --import ./test/test-store-env.mjs --test --test-concurrency=1 test/*.test.js && npm run test:verification:l3",
|
|
22
|
-
"test:verification:l1": "npm run build && node --test test/verification/harness.test.mjs test/verification/contract.test.mjs test/verification/graph-inventory.test.mjs test/verification/dashboard-drilldown.test.mjs test/verification/evidence-matrix.test.mjs",
|
|
23
|
-
"test:verification:l3": "npm run build && node --test --test-concurrency=1 test/verification/harness.test.mjs test/verification/l3-sqlite.test.mjs test/verification/l3-ingress.test.mjs test/verification/l3-replay.test.mjs test/verification/objective-authority.test.mjs test/verification/l3-convergence.test.mjs test/verification/l3-review-convergence.test.mjs test/verification/l3-human-escalation.test.mjs test/verification/l3-escalation-notifier.test.mjs test/verification/l3-unknown-effect.test.mjs test/verification/l3-cli-actions.test.mjs test/verification/l3-planner-round.test.mjs test/verification/full-recovery-gates.test.mjs test/verification/production-wiring.test.mjs test/verification/l3-full-path.test.mjs test/verification/l3-projection-recovery.test.mjs",
|
|
22
|
+
"test:verification:l1": "npm run build && node --test test/verification/harness.test.mjs test/verification/contract.test.mjs test/verification/graph-inventory.test.mjs test/verification/dashboard-drilldown.test.mjs test/verification/evidence-matrix.test.mjs test/verification/task-resolution-classification.test.mjs",
|
|
23
|
+
"test:verification:l3": "npm run build && node --test --test-concurrency=1 test/verification/harness.test.mjs test/verification/l3-sqlite.test.mjs test/verification/l3-ingress.test.mjs test/verification/l3-replay.test.mjs test/verification/objective-authority.test.mjs test/verification/l3-convergence.test.mjs test/verification/l3-review-convergence.test.mjs test/verification/l3-human-escalation.test.mjs test/verification/l3-escalation-notifier.test.mjs test/verification/l3-unknown-effect.test.mjs test/verification/l3-cli-actions.test.mjs test/verification/l3-planner-round.test.mjs test/verification/l3-task-resolution.test.mjs test/verification/full-recovery-gates.test.mjs test/verification/production-wiring.test.mjs test/verification/l3-full-path.test.mjs test/verification/l3-projection-recovery.test.mjs",
|
|
24
24
|
"test:verification": "npm run test:verification:l1 && npm run test:verification:l2 && npm run test:verification:l3 && npm run test:verification:cross-a && node --test test/workflow-production-outbox.test.js && npm run test:verification:matrix",
|
|
25
25
|
"e2e": "node e2e/runner/run.mjs",
|
|
26
26
|
"test:verification:cross-a": "npm run build && RELAY_A_DIST=$PWD/dist node --test test/verification/action-ledger-cross-branch.test.mjs test/verification/outbox-cross-a.test.mjs",
|
|
@@ -4,7 +4,7 @@ You are the default objective planner for Agents Relay autonomous jobs.
|
|
|
4
4
|
|
|
5
5
|
Before producing tasks, read and follow `../contracts/task-job-contract.md`.
|
|
6
6
|
|
|
7
|
-
You are a planning agent. Relay invokes you with the durable Job, existing Tasks, previous planner rounds, and resolved project context. Your sole responsibility is to decompose the objective and create a valid Plan/Task Tree.
|
|
7
|
+
You are a planning agent. Relay invokes you with the durable Job, existing Tasks, previous planner rounds, and resolved project context. Your sole responsibility is to decompose the objective and create a valid Plan/Task Tree. Every proposal MUST be committed with the provided `agents-relay plan apply` CLI command. Write the proposed JSON to a temporary file and run that command with the exact Plan and execution IDs from `plan_execution` / `apply_cli`. If the CLI rejects it, correct the reported error and resubmit within the SAME Plan, up to three corrections. Do not edit SQLite or other durable storage directly. Do not treat a JSON/text response as a plan submission and do not return the proposal as your final answer. After a successful CLI action, return exactly `PLAN_APPLIED:<plan-id>:<execution-id>` using those exact IDs. Never invent that receipt. If no live execution identity or CLI action is available, fail rather than returning a proposal.
|
|
8
8
|
|
|
9
9
|
Relay persists each planning round as a first-class Plan PR comment/marker before generated Task comments. The Plan carries its trigger/context (including unblock/change facts when applicable), assessment, and generated Task IDs. Legacy `kind=planner` Task records may still exist as execution history, but they are not the durable semantic container for the planning decision. Returned work Tasks are not automatically children of the Plan; use `parentTaskId` only when the business Task graph needs hierarchy.
|
|
10
10
|
|
|
@@ -96,7 +96,7 @@ Do not create separate Tasks merely for "analyze", "format", "call tool", "check
|
|
|
96
96
|
- If satisfied, return `objective_status: "satisfied"` and `next_tasks: []`.
|
|
97
97
|
- Otherwise return only the next useful executable batch from the graph-derived Task boundaries.
|
|
98
98
|
- Do not repeat completed work or recreate an existing Task unless replacement work is explicitly needed.
|
|
99
|
-
- **Resolution is mandatory for replacement work:** before returning any `next_tasks`, inspect every existing `FAILED` or `BLOCKED` work Task. If it lacks an active/waiting `resolution` and the proposed batch creates related/sibling work, explicitly cover it with a valid `resolution` on the new recovery Task. For a same-Job recovery child, use `"resolution":{"path":"same_job_dependency","targetTaskId":"<existing failed/blocked task ID>","rootCause":"<confirmed cause or stated uncertainty>","solution":"<how the new task resolves the original>"}`. `targetTaskId` identifies the **existing unresolved Task**, not the new Task. Keep the recovered work semantically under the original artifact's subtree. Do not invent a resolution or silently cancel unrelated failed work.
|
|
99
|
+
- **Resolution is mandatory for replacement work:** before returning any `next_tasks`, inspect every existing `FAILED` or `BLOCKED` work Task. If it lacks an active/waiting `resolution` or durable `resolutionClassification` and the proposed batch creates related/sibling work, explicitly cover it with a valid `resolution` on the new recovery Task. A superseded/invalid classification covers only planning validation; preserve the FAILED state and never infer success from the classification. For a same-Job recovery child, use `"resolution":{"path":"same_job_dependency","targetTaskId":"<existing failed/blocked task ID>","rootCause":"<confirmed cause or stated uncertainty>","solution":"<how the new task resolves the original>"}`. `targetTaskId` identifies the **existing unresolved Task**, not the new Task. Keep the recovered work semantically under the original artifact's subtree. Do not invent a resolution or silently cancel unrelated failed work.
|
|
100
100
|
- **On `retry_feedback`:** treat the feedback as Relay's authoritative validation error for the previous output in this **same Plan**. Correct the JSON proposal and all uncovered resolution targets; do not replay the same sibling Tasks unchanged. Preserve the intent and evidence already found, and return one fully corrected result. If no safe resolution can be justified, do not invent a successful outcome.
|
|
101
101
|
|
|
102
102
|
- Prefer independently verifiable Tasks with stable IDs, but do not split work solely to make Tasks smaller.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: troubleshooter
|
|
3
|
-
description: Recover
|
|
3
|
+
description: Recover failed Agents Relay tasks by diagnosing execution faults and repairing recoverable causes. Only a Troubleshooter can wait in BLOCKED; after authoritative unblock, the same Troubleshooter reruns and re-verifies before issuing a fresh resolution.
|
|
4
4
|
capabilities: [troubleshoot, recovery, orchestration, local_repo, iterative_tools]
|
|
5
5
|
runtime: codex
|
|
6
6
|
delegation: [p0-self-job, relay-task-retry]
|
|
@@ -9,7 +9,7 @@ restrictions: [preserve-failure-evidence, no-blind-retry, no-human-blocker-bypas
|
|
|
9
9
|
|
|
10
10
|
# Troubleshooter Agent
|
|
11
11
|
|
|
12
|
-
You are the recovery orchestrator for a FAILED Agents Relay task.
|
|
12
|
+
You are the recovery orchestrator for a FAILED Agents Relay task. Ordinary Tasks never enter BLOCKED or UNBLOCKED. Only a Troubleshooter can become BLOCKED, and only an authoritative unblock can resume that same Troubleshooter.
|
|
13
13
|
|
|
14
14
|
Your goal is not to explain the failure. Your goal is to make the original task move again when it is safe and technically possible.
|
|
15
15
|
|
|
@@ -33,7 +33,9 @@ Read `skills/agents-relay/troubleshooter-qa.md` for user-editable best practices
|
|
|
33
33
|
- For a transient failure, verify the transient condition is gone.
|
|
34
34
|
- For task-local failure, make the smallest durable repair in the original Job/PR when that is within scope.
|
|
35
35
|
- For a shared platform/runtime defect, first create or reuse the owning blocker P0/self-job. If it can be completed inside this recovery execution, fix/verify it and retry the original Task. If it cannot be completed now, register the original Task with `agents-relay blocker add` on that blocker Job and finish this Troubleshooter task; do not stay running while waiting for another Job.
|
|
36
|
-
- For a suspected genuine human/external blocker,
|
|
36
|
+
- For a suspected genuine human/external blocker, invoke `agents-relay escalation` with the original intent, blocker, and relevant context. The canonical graph owns the `resolved`, `failed`, and `needs_user` outcomes. When it returns `needs_user`, call Relay's `task escalate` operation with the concrete required action, resolution condition, and diagnostic context. Relay records the escalation and blocks this Troubleshooter; no worker remains running while waiting.
|
|
37
|
+
- After unblock, this same Troubleshooter must run with a new execution ID, re-check the blocking condition, and produce a fresh resolution bound to that execution. A previously resolved resolution record never authorizes retrying a new attempt.
|
|
38
|
+
- Unknown worker execution outcomes fail closed. Do not automatically reroute or retry the original task, and do not block the original task; diagnose it through the Troubleshooter.
|
|
37
39
|
7. For an immediate repair, only after the blocker is removed request `agents-relay task recover` with this Troubleshooter's recovery Task ID, then reconcile the original Job. Verify the SQLite postcondition: original Task state QUEUED, expected resolution and recovery correlation. An authorized human response is a Task Comment, not a rewrite of Task input. Retain historical conditions as provenance, independently verify the original failing execution context, and do not submit an identical escalation with no new evidence. Deferred repair uses the durable blocker relation: merge-mode blockers wake dependents after merge; release/deploy/runtime blockers require `--resolution verified` and `agents-relay blocker resolve` only after verification.
|
|
38
40
|
8. Verify durable evidence the original Task moved: at minimum QUEUED after retry, preferably a new execution / task.started / observable progress.
|
|
39
41
|
9. Publish this Troubleshooter Task's terminal event only after the recovery outcome is durable.
|
|
@@ -70,7 +70,7 @@ Keep these concepts distinct: observation is not root cause; hypothesis is not r
|
|
|
70
70
|
|
|
71
71
|
### Job-defined Task tag
|
|
72
72
|
|
|
73
|
-
`Task.state` is reserved for Relay lifecycle only: `DRAFT`, `QUEUED`, `RUNNING`, `BLOCKED`, `SUCCEEDED`, `FAILED`, and `CANCELLED`. A Job may additionally define one optional `tag` contract for a Job-specific result or classification that should be visible on each Task without changing its lifecycle state.
|
|
73
|
+
`Task.state` is reserved for Relay lifecycle only: `DRAFT`, `QUEUED`, `RUNNING`, `BLOCKED`, `SUCCEEDED`, `FAILED`, and `CANCELLED`. `BLOCKED` is exclusive to Tasks whose `agentName` is exactly `troubleshooter`; ordinary Tasks never enter `BLOCKED` or an `UNBLOCKED` lifecycle state. A Job may additionally define one optional `tag` contract for a Job-specific result or classification that should be visible on each Task without changing its lifecycle state.
|
|
74
74
|
|
|
75
75
|
The Job owns the meaning of the tag. Its trusted Job marker declares a human-readable `description` explaining what the tag means and when workers should set it, plus the allowed durable values and their dashboard display strings:
|
|
76
76
|
|
|
@@ -114,6 +114,18 @@ A tag must describe something specific to the Job contract, not Relay lifecycle.
|
|
|
114
114
|
For the ChatGPT Browser Worker guardian, for example, the Job defines `pass -> ✅` and `fail -> ❌`; each hourly E2E child Task reports `data.tag` after running the fixed deterministic E2E, while the Task lifecycle independently records whether Relay execution itself completed, failed, or became blocked.
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
### Task resolution classifications
|
|
118
|
+
|
|
119
|
+
`Task.resolutionClassification` is durable diagnostic metadata, separate from
|
|
120
|
+
`Task.state` and the recovery `Task.resolution` path. The canonical Task
|
|
121
|
+
Resolution Graph and `task classify` CLI action may classify a known FAILED
|
|
122
|
+
work Task as `superseded` or `invalid`; each record requires a different
|
|
123
|
+
durable target Task, evidence, and reason. Classification never changes the
|
|
124
|
+
FAILED lifecycle state, execution history, or result and never proves success.
|
|
125
|
+
Unknown outcomes, active executions, and Tasks with active recovery ownership
|
|
126
|
+
cannot be classified. Planners must preserve these fields in their assessment
|
|
127
|
+
and may treat classified history as covered when validating a fresh Plan.
|
|
128
|
+
|
|
117
129
|
### Optional Review Chair Tasks
|
|
118
130
|
|
|
119
131
|
Projects may use the shared `review-chair` agent as a normal Task or Agent
|
|
@@ -164,14 +176,14 @@ forbids it.
|
|
|
164
176
|
|
|
165
177
|
## Dependency vs block and authoritative unblock
|
|
166
178
|
|
|
167
|
-
`dependencies` describe normal DAG ordering. Dependency completion does not create an unblock lifecycle event or Planner unblock trigger; Relay
|
|
179
|
+
`dependencies` describe normal DAG ordering. An unmet dependency keeps ordinary work in `DRAFT`; dispatchable work is `QUEUED`. Dependency completion does not create an unblock lifecycle event or Planner unblock trigger; Relay releases dependent work when its prerequisites succeed. Worker, platform, semantic, and unknown-outcome failures are `FAILED`. Unknown worker outcome fails closed and never justifies rerouting. Only a Troubleshooter waiting for an authoritative human response may become `BLOCKED`.
|
|
168
180
|
|
|
169
|
-
A BLOCKED
|
|
181
|
+
A BLOCKED Troubleshooter cannot be resumed through retry, queue, dashboard state editing, PR push, webhook, ordinary task update, or Troubleshooter inference. The authoritative entry point records the response and transitions that same Troubleshooter `BLOCKED -> QUEUED` through `task.unblocked`. The queued Troubleshooter must execute again, verify the condition from fresh evidence, and issue a fresh resolution before Relay may retry or replan the original FAILED Task. Prior `resolved` metadata never authorizes another attempt. Historical ordinary BLOCKED snapshots migrate to FAILED while their append-only event history remains unchanged; they are never silently converted to success.
|
|
170
182
|
|
|
171
|
-
|
|
183
|
+
The old `UNBLOCKED` state is compatibility-only input. It normalizes to QUEUED only for Troubleshooter Tasks and to FAILED for ordinary Tasks, pending diagnosis.
|
|
172
184
|
|
|
173
185
|
Each Planner round is also a first-class durable Plan entity stored in its own `agents-relay:plan:v1` PR comment. Relay creates the Plan comment before the internal planner execution record and before any generated Task comments, then updates that same Plan comment with the final assessment and generated Task IDs. Unblock/change facts belong to the Plan trigger/context, not to new Job-level Change Cycle state. Historical `kind=planner` Task records and legacy Job `changeCycle` fields remain readable for compatibility only.
|
|
174
186
|
|
|
175
|
-
A blocker Job owns reverse `unblocks` references. When it resolves, Relay
|
|
187
|
+
A blocker Job owns reverse `unblocks` references. When it resolves, Relay releases matching dependency-waiting Tasks through their ordinary DRAFT/QUEUED path. It cannot unblock ordinary work or resume a Troubleshooter without the authoritative response flow.
|
|
176
188
|
|
|
177
|
-
Automatic Planner creation treats
|
|
189
|
+
Automatic Planner creation treats a currently BLOCKED Troubleshooter as a recovery gate. FAILED history alone does not authorize resolution or success; active DRAFT/QUEUED/RUNNING work still prevents a competing planning round.
|
|
@@ -5,15 +5,12 @@ description: Validates authoritative unblock requests and safely resumes blocked
|
|
|
5
5
|
flowchart LR
|
|
6
6
|
%% relay: initial blocked
|
|
7
7
|
blocked["state: blocked"]
|
|
8
|
-
|
|
9
|
-
fixed["state: fixed_retry"]
|
|
8
|
+
queued["state: troubleshooter_queued"]
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
replan["action: unblock.replan"]
|
|
10
|
+
eligible["guard: blockedTroubleshooter"]
|
|
13
11
|
retry["action: unblock.retry"]
|
|
14
12
|
|
|
15
|
-
blocked -->|on: task.authoritative_unblock|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
retry --> fixed
|
|
13
|
+
blocked -->|on: task.authoritative_unblock| eligible
|
|
14
|
+
eligible -->|true| retry
|
|
15
|
+
eligible -->|false| blocked
|
|
16
|
+
retry --> queued
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Blocker Resolution
|
|
3
|
-
description:
|
|
3
|
+
description: Releases dependency-waiting work; failed work and blocked Troubleshooters require their own fresh recovery flow.
|
|
4
4
|
---
|
|
5
5
|
flowchart LR
|
|
6
6
|
%% relay: initial waiting
|
|
@@ -27,12 +27,7 @@ isBlocked["guard: isBlocked"]
|
|
|
27
27
|
isFailed["guard: isFailed"]
|
|
28
28
|
isUnblocked["guard: isUnblocked"]
|
|
29
29
|
|
|
30
|
-
autonomousUnblock["action: blocker.autonomousUnblock"]
|
|
31
|
-
acceptExisting["action: blocker.acceptExistingUnblock"]
|
|
32
30
|
releaseDraft["action: blocker.releaseDraft"]
|
|
33
|
-
unblockTask["action: blocker.unblockTask"]
|
|
34
|
-
retryFailed["action: blocker.retryFailed"]
|
|
35
|
-
retryUnblocked["action: blocker.retryUnblocked"]
|
|
36
31
|
|
|
37
32
|
waiting -->|on: blocker.resolved| resolutionAllowed
|
|
38
33
|
resolutionAllowed -->|false| wait
|
|
@@ -47,13 +42,11 @@ autonomousJob -->|true| checkEffective
|
|
|
47
42
|
autonomousJob -->|false| checkDraft
|
|
48
43
|
|
|
49
44
|
checkEffective -->|on: blocker.resolved| effectivelyBlocked
|
|
50
|
-
effectivelyBlocked -->|true|
|
|
51
|
-
autonomousUnblock --> resumed
|
|
45
|
+
effectivelyBlocked -->|true| wait
|
|
52
46
|
effectivelyBlocked -->|false| checkAlreadyUnblocked
|
|
53
47
|
|
|
54
48
|
checkAlreadyUnblocked -->|on: blocker.resolved| alreadyUnblocked
|
|
55
|
-
alreadyUnblocked -->|true|
|
|
56
|
-
acceptExisting --> resumed
|
|
49
|
+
alreadyUnblocked -->|true| resumed
|
|
57
50
|
alreadyUnblocked -->|false| checkDraft
|
|
58
51
|
|
|
59
52
|
checkDraft -->|on: blocker.resolved| isDraft
|
|
@@ -62,16 +55,13 @@ releaseDraft --> resumed
|
|
|
62
55
|
isDraft -->|false| checkBlocked
|
|
63
56
|
|
|
64
57
|
checkBlocked -->|on: blocker.resolved| isBlocked
|
|
65
|
-
isBlocked -->|true|
|
|
66
|
-
unblockTask --> resumed
|
|
58
|
+
isBlocked -->|true| wait
|
|
67
59
|
isBlocked -->|false| checkFailed
|
|
68
60
|
|
|
69
61
|
checkFailed -->|on: blocker.resolved| isFailed
|
|
70
|
-
isFailed -->|true|
|
|
71
|
-
retryFailed --> resumed
|
|
62
|
+
isFailed -->|true| wait
|
|
72
63
|
isFailed -->|false| checkUnblocked
|
|
73
64
|
|
|
74
65
|
checkUnblocked -->|on: blocker.resolved| isUnblocked
|
|
75
|
-
isUnblocked -->|true|
|
|
76
|
-
retryUnblocked --> resumed
|
|
66
|
+
isUnblocked -->|true| wait
|
|
77
67
|
isUnblocked -->|false| obsolete
|
|
@@ -7,7 +7,7 @@ flowchart LR
|
|
|
7
7
|
idle["state: idle"]
|
|
8
8
|
check["state: check"]
|
|
9
9
|
external["state: external_completed"]
|
|
10
|
-
|
|
10
|
+
failed["state: failed"]
|
|
11
11
|
skip["state: skip"]
|
|
12
12
|
rescued["state: rescued"]
|
|
13
13
|
legacyActive["guard: legacyActive"]
|
|
@@ -16,7 +16,7 @@ unknownStartedOutcome["guard: unknownStartedOutcome"]
|
|
|
16
16
|
rescuable["guard: rescuable"]
|
|
17
17
|
rescue["action: task.rescueOrchestrator"]
|
|
18
18
|
externalCompletion["action: task.applyExternalCompletion"]
|
|
19
|
-
quarantine["action: task.
|
|
19
|
+
quarantine["action: task.failLegacyOrchestrator"]
|
|
20
20
|
idle -->|on: orchestrator.rescue.reconcile| check
|
|
21
21
|
check -->|on: orchestrator.rescue.reconcile| legacyActive
|
|
22
22
|
legacyActive -->|false| skip
|
|
@@ -25,7 +25,7 @@ verifiedExternalCompletion -->|true| externalCompletion
|
|
|
25
25
|
externalCompletion -->|to: external_completed| external
|
|
26
26
|
verifiedExternalCompletion -->|false| unknownStartedOutcome
|
|
27
27
|
unknownStartedOutcome -->|true| quarantine
|
|
28
|
-
quarantine -->|to:
|
|
28
|
+
quarantine -->|to: failed| failed
|
|
29
29
|
unknownStartedOutcome -->|false| rescuable
|
|
30
30
|
rescuable -->|false| skip
|
|
31
31
|
rescuable -->|true| rescue
|