agents-relay 2.0.155 → 2.0.157
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/adapters.js +2 -2
- package/dist/blockers.js +6 -27
- package/dist/chatgpt-platform-fallback.js +1 -1
- package/dist/cli-help.js +1 -1
- package/dist/cli-unblock.js +3 -14
- package/dist/dashboard-actions.js +9 -19
- package/dist/dashboard-data.js +45 -0
- 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/planner.js +1 -1
- package/dist/projection-outbox.js +1 -1
- package/dist/reconciler-runtime.js +15 -19
- package/dist/relay-store.js +2 -0
- package/dist/relayd-control.js +2 -5
- package/dist/task-actions.js +9 -3
- package/dist/task-recovery.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 +21 -13
- package/dist/workflow/troubleshooter-v2.js +5 -3
- package/package.json +1 -1
- package/skills/agents-relay/agents/troubleshooter.agent.md +5 -3
- package/skills/agents-relay/contracts/task-job-contract.md +6 -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 +9 -3
- package/workflow/troubleshooter.mmd +20 -1
package/dist/types.js
CHANGED
|
@@ -5,13 +5,19 @@ export function taskOverallTimeoutMs(size, reasoning) { return storySizeTimeoutM
|
|
|
5
5
|
export function chatGptStartAckTimeoutMs(overallTimeoutMs) { return Math.min(600000, Math.max(120000, Math.floor(overallTimeoutMs / 3))); }
|
|
6
6
|
/** Normalize snapshots written before UNBLOCKED was removed from the lifecycle graph. */
|
|
7
7
|
export function normalizeLegacyTaskLifecycle(task, executionMode) {
|
|
8
|
+
void executionMode;
|
|
8
9
|
const legacy = task;
|
|
9
|
-
if (legacy.state
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
if (legacy.state === 'UNBLOCKED') {
|
|
11
|
+
legacy.state = task.agentName === 'troubleshooter' ? 'QUEUED' : 'FAILED';
|
|
12
|
+
task.failureReason ??= 'legacy_unblocked_task_requires_recovery';
|
|
13
|
+
task.maxAttempts = Math.max(3, task.maxAttempts ?? 0, task.attempt + 1);
|
|
14
|
+
task.unblock = task.unblock ? { ...task.unblock, event: 'task.unblocked' } : undefined;
|
|
15
|
+
}
|
|
16
|
+
if (legacy.state === 'BLOCKED' && task.kind !== 'planner' && task.agentName !== 'troubleshooter') {
|
|
17
|
+
legacy.state = 'FAILED';
|
|
18
|
+
task.failureReason ??= task.failureReason === 'worker_outcome_unknown' ? task.failureReason : 'legacy_ordinary_blocked';
|
|
19
|
+
task.error ??= 'Legacy ordinary Task was BLOCKED; preserved as FAILED pending Troubleshooter diagnosis.';
|
|
20
|
+
}
|
|
15
21
|
return task;
|
|
16
22
|
}
|
|
17
23
|
export function assertJobLifecycleTransition(from, to) { assertV2StateTransition(jobLifecycleV2.graph, from, to, 'job'); }
|
package/dist/unblock.js
CHANGED
|
@@ -2,7 +2,7 @@ import { applyTaskUnblockRetrySnapshot } from './task-actions.js';
|
|
|
2
2
|
import { tryTransitionTaskEvent } from './scheduler.js';
|
|
3
3
|
import { dispatchAuthoritativeUnblock } from './workflow/authoritative-unblock-v2.js';
|
|
4
4
|
import { hasPendingTroubleshooterEscalation } from './task-recovery.js';
|
|
5
|
-
export function isEffectivelyBlocked(task) { return task.kind !== 'planner' && task.state === 'BLOCKED'; }
|
|
5
|
+
export function isEffectivelyBlocked(task) { return task.kind !== 'planner' && task.agentName === 'troubleshooter' && task.state === 'BLOCKED'; }
|
|
6
6
|
function assertNoPendingHumanEscalation(tasks) {
|
|
7
7
|
if (tasks.some(task => task.agentName === 'troubleshooter' && task.escalation?.status === 'pending'))
|
|
8
8
|
throw new Error('Blocked Troubleshooter requires an authorized human response; use task unblock --response');
|
|
@@ -32,7 +32,7 @@ function unblockFacts(job, tasks, currentHeadSha, reason) {
|
|
|
32
32
|
}
|
|
33
33
|
export function unconsumedUnblockTrigger(job) {
|
|
34
34
|
const consumed = new Set((job.plans ?? []).filter(plan => plan.status === 'SUCCEEDED').flatMap(plan => plan.trigger.source === 'unblock' ? plan.trigger.taskIds ?? [] : []));
|
|
35
|
-
const tasks = job.tasks.filter(task => task.unblock && !consumed.has(task.id) && ['DRAFT', 'QUEUED'
|
|
35
|
+
const tasks = job.tasks.filter(task => task.agentName !== 'troubleshooter' && task.unblock && !consumed.has(task.id) && ['DRAFT', 'QUEUED'].includes(task.state));
|
|
36
36
|
if (tasks.length === 0) {
|
|
37
37
|
const legacy = job.changeCycle;
|
|
38
38
|
if (!legacy || legacy.consumedByPlannerTaskId)
|
|
@@ -60,43 +60,8 @@ export function unblockNeedsNewPlan(job) {
|
|
|
60
60
|
&& latest.trigger.taskIds?.some(id => pending.taskIds?.includes(id)));
|
|
61
61
|
}
|
|
62
62
|
export async function unblockAutonomousTasks(options) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (tasks.length === 0)
|
|
66
|
-
throw new Error('No effective BLOCKED work task is available to unblock');
|
|
67
|
-
assertNoPendingHumanEscalation(tasks);
|
|
68
|
-
const now = options.now ?? new Date().toISOString();
|
|
69
|
-
const currentHeadSha = await store.headSha?.() ?? job.headSha ?? null;
|
|
70
|
-
const facts = unblockFacts(job, tasks, currentHeadSha, trigger.reason);
|
|
71
|
-
const apply = (current) => {
|
|
72
|
-
for (const requested of tasks) {
|
|
73
|
-
const task = current.tasks.find(item => item.id === requested.id);
|
|
74
|
-
if (!task || !isEffectivelyBlocked(task))
|
|
75
|
-
throw new Error(`Task ${requested.id} changed before authoritative unblock`);
|
|
76
|
-
const baseline = baselineFor(task);
|
|
77
|
-
if (!tryTransitionTaskEvent(task, 'task.unblocked', { autonomousJob: true }).accepted)
|
|
78
|
-
throw new Error(`Task ${task.id} cannot be authoritatively unblocked`);
|
|
79
|
-
task.blockedBaseline = baseline;
|
|
80
|
-
task.unblock = { event: 'task.unblocked', at: now, source: trigger.source, reason: trigger.reason, baseline, currentHeadSha: currentHeadSha ?? undefined, facts, blockerJobId: trigger.blockerJobId, resolution: trigger.resolution };
|
|
81
|
-
task.updatedAt = now;
|
|
82
|
-
delete task.blockedBy;
|
|
83
|
-
}
|
|
84
|
-
current.updatedAt = now;
|
|
85
|
-
};
|
|
86
|
-
if (store.mutateJob) {
|
|
87
|
-
const committed = await store.mutateJob(job.id, current => { apply(current); return current; });
|
|
88
|
-
Object.assign(job, structuredClone(committed));
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
apply(job);
|
|
92
|
-
for (const task of tasks)
|
|
93
|
-
await store.saveTask(task);
|
|
94
|
-
}
|
|
95
|
-
const planTrigger = unconsumedUnblockTrigger(job) ?? {
|
|
96
|
-
source: 'unblock', taskIds: tasks.map(task => task.id), reason: trigger.reason,
|
|
97
|
-
facts, currentHeadSha: currentHeadSha ?? undefined, unblockSource: trigger.source,
|
|
98
|
-
};
|
|
99
|
-
return planTrigger;
|
|
63
|
+
void options;
|
|
64
|
+
throw new Error('Ordinary Tasks cannot be authoritatively unblocked; only a blocked Troubleshooter can resume');
|
|
100
65
|
}
|
|
101
66
|
export async function unblockTask(options) {
|
|
102
67
|
if (!isEffectivelyBlocked(options.task))
|
|
@@ -106,11 +71,7 @@ export async function unblockTask(options) {
|
|
|
106
71
|
throw new Error(`Task ${options.task.id} cannot be directly unblocked while its Troubleshooter is awaiting human action`);
|
|
107
72
|
}
|
|
108
73
|
let result;
|
|
109
|
-
const decision = await dispatchAuthoritativeUnblock(options.
|
|
110
|
-
if (actionId === 'unblock.replan') {
|
|
111
|
-
result = { replanning: true, planTrigger: await unblockAutonomousTasks({ store: options.store, job: options.job, tasks: [options.task], trigger: options.trigger, now: options.now }) };
|
|
112
|
-
return {};
|
|
113
|
-
}
|
|
74
|
+
const decision = await dispatchAuthoritativeUnblock(options.task.agentName === 'troubleshooter', async (actionId) => {
|
|
114
75
|
if (actionId === 'unblock.retry') {
|
|
115
76
|
const now = options.now ?? new Date().toISOString();
|
|
116
77
|
const currentHeadSha = await options.store.headSha?.() ?? options.job.headSha ?? null;
|
|
@@ -4,17 +4,16 @@ import { dispatchWorkflowGraph, dispatchWorkflowGraphSync } from './dispatcher.j
|
|
|
4
4
|
const source = readFileSync(new URL('../../workflow/authoritative-unblock.mmd', import.meta.url), 'utf8');
|
|
5
5
|
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
6
6
|
export const authoritativeUnblockV2 = new GraphRuntime(source, {
|
|
7
|
-
states: new Set(['blocked', '
|
|
8
|
-
guards: {
|
|
7
|
+
states: new Set(['blocked', 'troubleshooter_queued']),
|
|
8
|
+
guards: { blockedTroubleshooter: boolGuard('blockedTroubleshooter') },
|
|
9
9
|
actions: {
|
|
10
|
-
'unblock.replan': { id: 'unblock.replan' },
|
|
11
10
|
'unblock.retry': { id: 'unblock.retry' },
|
|
12
11
|
},
|
|
13
12
|
events: { 'task.authoritative_unblock': { id: 'task.authoritative_unblock' } },
|
|
14
13
|
});
|
|
15
|
-
export async function dispatchAuthoritativeUnblock(
|
|
16
|
-
return dispatchWorkflowGraph(authoritativeUnblockV2, { initialState: 'blocked', input: { transport: 'event', id: 'task.authoritative_unblock', payload: {} }, graphName: 'authoritative-unblock', durableFacts: {
|
|
14
|
+
export async function dispatchAuthoritativeUnblock(blockedTroubleshooter, executeAction) {
|
|
15
|
+
return dispatchWorkflowGraph(authoritativeUnblockV2, { initialState: 'blocked', input: { transport: 'event', id: 'task.authoritative_unblock', payload: {} }, graphName: 'authoritative-unblock', durableFacts: { blockedTroubleshooter }, executeAction });
|
|
17
16
|
}
|
|
18
|
-
export function decideAuthoritativeUnblock(
|
|
19
|
-
return dispatchWorkflowGraphSync(authoritativeUnblockV2, { initialState: 'blocked', input: { transport: 'event', id: 'task.authoritative_unblock', payload: {} }, graphName: 'authoritative-unblock', durableFacts: {
|
|
17
|
+
export function decideAuthoritativeUnblock(blockedTroubleshooter) {
|
|
18
|
+
return dispatchWorkflowGraphSync(authoritativeUnblockV2, { initialState: 'blocked', input: { transport: 'event', id: 'task.authoritative_unblock', payload: {} }, graphName: 'authoritative-unblock', durableFacts: { blockedTroubleshooter } });
|
|
20
19
|
}
|
|
@@ -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,11 +7,12 @@ 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', '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'),
|
|
15
16
|
},
|
|
16
17
|
actions: Object.fromEntries(['resolution.activateRecovery', 'resolution.attachSameJobDependency', 'resolution.registerBlockerDependency', 'resolution.createEscalationGate', 'resolution.cancelSupersede'].map(id => [id, { id }])),
|
|
17
18
|
events: { 'resolution.propose': { id: 'resolution.propose' } },
|
|
@@ -22,8 +23,8 @@ export function validateResolutionProposal(job, original, proposal) {
|
|
|
22
23
|
throw new Error(`Unknown task resolution path ${proposal.path}`);
|
|
23
24
|
if (proposal.path === 'exact_original_retry' && original.state !== 'FAILED')
|
|
24
25
|
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');
|
|
26
|
+
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))))
|
|
27
|
+
throw new Error('Same-Job dependency resolution requires an existing unresolved target Task in the same Job');
|
|
27
28
|
if (proposal.path === 'blocker_job_dependency' && (!proposal.targetJobId || !proposal.mode))
|
|
28
29
|
throw new Error('Blocker-Job dependency resolution requires targetJobId and merge or verified mode');
|
|
29
30
|
if (proposal.path === 'escalation_gate' && (!proposal.rootCause || !proposal.solution))
|
|
@@ -32,10 +33,15 @@ export function validateResolutionProposal(job, original, proposal) {
|
|
|
32
33
|
throw new Error('Cancellation/supersession requires an explicit obsolescence rootCause');
|
|
33
34
|
}
|
|
34
35
|
export function resolutionFacts(task, proposal, owner) {
|
|
35
|
-
|
|
36
|
+
const freshRecoveryExecution = owner !== 'troubleshooter' || proposal.path !== 'exact_original_retry' || (Boolean(proposal.recoveryExecutionId)
|
|
37
|
+
&& proposal.recoveryExecutionId !== task.resolution?.recoveryExecutionId);
|
|
38
|
+
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
39
|
}
|
|
37
40
|
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 =>
|
|
41
|
+
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 }));
|
|
42
|
+
}
|
|
43
|
+
function checkedResolutionDecision(decision) {
|
|
44
|
+
return decision.accepted && decision.target === 'rejected' ? { ...decision, accepted: false, reason: 'no_transition' } : decision;
|
|
39
45
|
}
|
|
40
46
|
export function dispatchTaskResolutionSync(task, proposal, owner, effects) {
|
|
41
47
|
const handlers = {
|
|
@@ -45,13 +51,13 @@ export function dispatchTaskResolutionSync(task, proposal, owner, effects) {
|
|
|
45
51
|
'resolution.createEscalationGate': effects.createEscalationGate,
|
|
46
52
|
'resolution.cancelSupersede': effects.cancelSupersede,
|
|
47
53
|
};
|
|
48
|
-
return dispatchWorkflowGraphSync(taskResolutionV2, {
|
|
54
|
+
return checkedResolutionDecision(dispatchWorkflowGraphSync(taskResolutionV2, {
|
|
49
55
|
initialState: 'unresolved', input: { transport: 'event', id: 'resolution.propose', payload: proposal },
|
|
50
56
|
durableFacts: resolutionFacts(task, proposal, owner), graphName: 'task-resolution',
|
|
51
|
-
isTransientState: state =>
|
|
57
|
+
isTransientState: state => ['check_owner', 'check_path', 'check_fresh_execution'].includes(state), maxSteps: 8,
|
|
52
58
|
executeAction: actionId => { const handler = handlers[actionId]; if (!handler)
|
|
53
59
|
throw new Error(`Task resolution effect ${actionId} is unavailable`); handler(); return {}; },
|
|
54
|
-
});
|
|
60
|
+
}));
|
|
55
61
|
}
|
|
56
62
|
export async function dispatchTaskResolution(task, proposal, owner, effects) {
|
|
57
63
|
const handlers = {
|
|
@@ -61,13 +67,13 @@ export async function dispatchTaskResolution(task, proposal, owner, effects) {
|
|
|
61
67
|
'resolution.createEscalationGate': effects.createEscalationGate,
|
|
62
68
|
'resolution.cancelSupersede': effects.cancelSupersede,
|
|
63
69
|
};
|
|
64
|
-
return dispatchWorkflowGraph(taskResolutionV2, {
|
|
70
|
+
return checkedResolutionDecision(await dispatchWorkflowGraph(taskResolutionV2, {
|
|
65
71
|
initialState: 'unresolved',
|
|
66
72
|
input: { transport: 'event', id: 'resolution.propose', payload: proposal },
|
|
67
73
|
durableFacts: resolutionFacts(task, proposal, owner),
|
|
68
74
|
graphName: 'task-resolution',
|
|
69
|
-
isTransientState: state =>
|
|
70
|
-
maxSteps:
|
|
75
|
+
isTransientState: state => ['check_owner', 'check_path', 'check_fresh_execution'].includes(state),
|
|
76
|
+
maxSteps: 8,
|
|
71
77
|
executeAction: async (actionId) => {
|
|
72
78
|
const handler = handlers[actionId];
|
|
73
79
|
if (!handler)
|
|
@@ -75,7 +81,7 @@ export async function dispatchTaskResolution(task, proposal, owner, effects) {
|
|
|
75
81
|
await handler();
|
|
76
82
|
return {};
|
|
77
83
|
},
|
|
78
|
-
});
|
|
84
|
+
}));
|
|
79
85
|
}
|
|
80
86
|
export function materializeResolution(task, proposal, owner, now = new Date().toISOString()) {
|
|
81
87
|
return { ...proposal, state: proposal.path === 'escalation_gate' || proposal.path === 'blocker_job_dependency' ? 'waiting' : 'active', owner, updatedAt: now };
|
|
@@ -100,5 +106,7 @@ export function attachResolutionOwnership(job, child, proposal, owner) {
|
|
|
100
106
|
return original;
|
|
101
107
|
}
|
|
102
108
|
export function claimExactOriginalRecovery(original, recovery) {
|
|
103
|
-
|
|
109
|
+
if (!recovery.executionId)
|
|
110
|
+
throw new Error(`Troubleshooter ${recovery.id} has no current execution for a fresh resolution`);
|
|
111
|
+
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
112
|
}
|
|
@@ -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: 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
|
|
|
@@ -164,14 +164,14 @@ forbids it.
|
|
|
164
164
|
|
|
165
165
|
## Dependency vs block and authoritative unblock
|
|
166
166
|
|
|
167
|
-
`dependencies` describe normal DAG ordering. Dependency completion does not create an unblock lifecycle event or Planner unblock trigger; Relay
|
|
167
|
+
`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
168
|
|
|
169
|
-
A BLOCKED
|
|
169
|
+
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
170
|
|
|
171
|
-
|
|
171
|
+
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
172
|
|
|
173
173
|
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
174
|
|
|
175
|
-
A blocker Job owns reverse `unblocks` references. When it resolves, Relay
|
|
175
|
+
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
176
|
|
|
177
|
-
Automatic Planner creation treats
|
|
177
|
+
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
|