agents-relay 1.0.90 → 1.0.92

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.
@@ -1,14 +1,28 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { parseExecutableMermaid, WorkflowEngine } from './mermaid.js';
3
- function load(name, guards = {}, actions = {}) {
3
+ import { WORKFLOW_EFFECT_NAMES, WORKFLOW_EVENT_NAMES, WORKFLOW_MUTATION_NAMES } from './contracts.js';
4
+ function declarativeActions() {
5
+ return Object.fromEntries([...WORKFLOW_MUTATION_NAMES, ...WORKFLOW_EFFECT_NAMES].map(name => [name, { declarative: true }]));
6
+ }
7
+ function load(name, guards = {}) {
4
8
  const url = new URL(`../../workflow/${name}.mmd`, import.meta.url);
5
- return new WorkflowEngine(parseExecutableMermaid(readFileSync(url, 'utf8')), guards, actions);
9
+ const graph = parseExecutableMermaid(readFileSync(url, 'utf8'));
10
+ const canonicalEvents = new Set(WORKFLOW_EVENT_NAMES);
11
+ for (const transition of graph.transitions) {
12
+ if (!canonicalEvents.has(transition.event)) {
13
+ throw new Error(`Unknown canonical workflow event ${transition.event} referenced at line ${transition.line}`);
14
+ }
15
+ }
16
+ return new WorkflowEngine(graph, guards, declarativeActions());
6
17
  }
7
18
  const booleanGuard = (name) => context => context[name] === true;
8
- const lifecycleAction = () => { };
9
- export const jobLifecycleWorkflow = load('job-lifecycle', { notMerged: booleanGuard('notMerged') }, { cancelJob: lifecycleAction });
19
+ export const jobLifecycleWorkflow = load('job-lifecycle', {
20
+ notMerged: booleanGuard('notMerged'),
21
+ autoMerge: booleanGuard('autoMerge'),
22
+ });
10
23
  export const taskLifecycleWorkflow = load('task-lifecycle', {
11
24
  dependenciesSatisfied: booleanGuard('dependenciesSatisfied'),
12
25
  dependenciesUnsatisfied: booleanGuard('dependenciesUnsatisfied'),
13
26
  runnable: booleanGuard('runnable'),
14
- }, { release: lifecycleAction, hold: lifecycleAction });
27
+ dispatchable: booleanGuard('dispatchable'),
28
+ });
@@ -0,0 +1,74 @@
1
+ export class MemoryWorkflowEffectReceiptStore {
2
+ completed = new Set();
3
+ async has(key) { return this.completed.has(key); }
4
+ async markCompleted(key) { this.completed.add(key); }
5
+ }
6
+ export class WorkflowEffectHandler {
7
+ name;
8
+ constructor(name) {
9
+ this.name = name;
10
+ }
11
+ idempotencyKey(context) {
12
+ return `${this.name}:${context.jobId}:${context.taskId ?? 'job'}:${context.transitionKey}`;
13
+ }
14
+ }
15
+ export class CallbackWorkflowEffectHandler extends WorkflowEffectHandler {
16
+ callback;
17
+ key;
18
+ constructor(name, callback, key) {
19
+ super(name);
20
+ this.callback = callback;
21
+ this.key = key;
22
+ }
23
+ idempotencyKey(context) {
24
+ return this.key?.(context) ?? super.idempotencyKey(context);
25
+ }
26
+ async execute(context) { await this.callback(context); }
27
+ }
28
+ export class WorkflowEffectExecutor {
29
+ receipts;
30
+ handlers = new Map();
31
+ constructor(handlers = [], receipts = new MemoryWorkflowEffectReceiptStore()) {
32
+ this.receipts = receipts;
33
+ for (const handler of handlers)
34
+ this.register(handler);
35
+ }
36
+ register(handler) {
37
+ if (this.handlers.has(handler.name))
38
+ throw new Error(`Workflow effect handler already registered: ${handler.name}`);
39
+ this.handlers.set(handler.name, handler);
40
+ }
41
+ async execute(effects, context, onOutcome) {
42
+ const outcomes = [];
43
+ for (const effect of effects) {
44
+ const handler = this.handlers.get(effect);
45
+ if (!handler)
46
+ throw new Error(`No workflow effect handler registered for ${effect}`);
47
+ const key = handler.idempotencyKey(context);
48
+ if (await this.receipts.has(key)) {
49
+ const outcome = { effect, key, status: 'deduplicated' };
50
+ outcomes.push(outcome);
51
+ await onOutcome?.(outcome);
52
+ continue;
53
+ }
54
+ try {
55
+ await handler.execute(context);
56
+ await this.receipts.markCompleted(key);
57
+ const outcome = { effect, key, status: 'completed' };
58
+ outcomes.push(outcome);
59
+ await onOutcome?.(outcome);
60
+ }
61
+ catch (error) {
62
+ const outcome = {
63
+ effect,
64
+ key,
65
+ status: 'failed',
66
+ error: error instanceof Error ? error.message : String(error),
67
+ };
68
+ outcomes.push(outcome);
69
+ await onOutcome?.(outcome);
70
+ }
71
+ }
72
+ return outcomes;
73
+ }
74
+ }
@@ -144,7 +144,10 @@ export class WorkflowEngine {
144
144
  throw new Error(`Ambiguous workflow transition from ${state} on ${event}`);
145
145
  const transition = enabled[0];
146
146
  for (const actionName of transition.actions) {
147
- const value = this.actions[actionName](context, transition);
147
+ const action = this.actions[actionName];
148
+ if (typeof action !== 'function')
149
+ continue;
150
+ const value = action(context, transition);
148
151
  if (value instanceof Promise)
149
152
  throw new Error(`Workflow action ${actionName} is asynchronous and cannot be used in a synchronous transition`);
150
153
  }
@@ -173,7 +176,8 @@ export class WorkflowEngine {
173
176
  const transition = enabled[0];
174
177
  for (const actionName of transition.actions) {
175
178
  const action = this.actions[actionName];
176
- await action(context, transition);
179
+ if (typeof action === 'function')
180
+ await action(context, transition);
177
181
  }
178
182
  return transition.to;
179
183
  }
@@ -1,4 +1,6 @@
1
1
  import { jobLifecycleWorkflow, taskLifecycleWorkflow } from './definitions.js';
2
+ import { classifyWorkflowActions, emptyWorkflowDecision, } from './contracts.js';
3
+ import { graphWorkflowDecision } from './runtime.js';
2
4
  let modeOverride = null;
3
5
  let reporter = null;
4
6
  let matches = 0;
@@ -13,108 +15,116 @@ export function parseWorkflowRolloutMode(value) {
13
15
  export function workflowRolloutMode() {
14
16
  return modeOverride ?? parseWorkflowRolloutMode(process.env.AGENTS_RELAY_WORKFLOW_MODE);
15
17
  }
16
- export function setWorkflowRolloutMode(mode) {
17
- modeOverride = mode;
18
- }
19
- export function setWorkflowShadowReporter(value) {
20
- reporter = value;
21
- }
22
- export function resetWorkflowRolloutDiagnostics() {
23
- matches = 0;
24
- mismatches = 0;
25
- }
18
+ export function setWorkflowRolloutMode(mode) { modeOverride = mode; }
19
+ export function setWorkflowShadowReporter(value) { reporter = value; }
20
+ export function resetWorkflowRolloutDiagnostics() { matches = 0; mismatches = 0; }
26
21
  export function workflowRolloutSnapshot() {
27
22
  return { mode: workflowRolloutMode(), matches, mismatches };
28
23
  }
29
- function graphDecision(engine, from, event, context) {
30
- const enabled = engine.enabledSync(from, event, context);
31
- if (enabled.length === 0)
32
- return { accepted: false, target: null, guard: null, actions: [], reason: 'no_transition' };
33
- if (enabled.length > 1)
34
- return { accepted: false, target: null, guard: null, actions: [], reason: 'ambiguous' };
35
- const transition = enabled[0];
24
+ function accepted(target, guard = null, actions = []) {
25
+ const actionList = [...actions];
36
26
  return {
37
27
  accepted: true,
38
- target: transition.to,
39
- guard: transition.guard,
40
- actions: [...transition.actions],
28
+ target,
29
+ guard,
30
+ actions: actionList,
31
+ ...classifyWorkflowActions(actionList),
41
32
  reason: 'accepted',
42
33
  };
43
34
  }
35
+ const CLEAR = ['mutation.clearError', 'mutation.clearFailureReason'];
44
36
  const taskLegacy = {
45
37
  DRAFT: {
46
- release: { target: 'QUEUED' },
38
+ release: { target: 'QUEUED', actions: CLEAR },
47
39
  block: { target: 'BLOCKED' },
48
40
  cancel: { target: 'CANCELLED' },
49
41
  },
50
42
  QUEUED: {
51
43
  hold: { target: 'DRAFT' },
52
- 'task.started': { target: 'RUNNING' },
44
+ 'task.started': { target: 'RUNNING', actions: CLEAR },
53
45
  block: { target: 'BLOCKED' },
54
46
  fail: { target: 'FAILED' },
47
+ 'effect.launchTask.failed': { target: 'FAILED' },
55
48
  cancel: { target: 'CANCELLED' },
56
49
  },
57
50
  RUNNING: {
58
51
  'task.blocked': { target: 'BLOCKED' },
59
- 'task.completed': { target: 'SUCCEEDED' },
52
+ 'task.completed': { target: 'SUCCEEDED', actions: CLEAR },
60
53
  'task.failed': { target: 'FAILED' },
61
54
  'task.cancelled': { target: 'CANCELLED' },
62
55
  },
63
56
  BLOCKED: {
64
- retry: { target: 'QUEUED' },
57
+ retry: { target: 'QUEUED', actions: CLEAR },
65
58
  cancel: { target: 'CANCELLED' },
66
59
  },
67
60
  SUCCEEDED: {},
68
61
  FAILED: {
69
- retry: { target: 'QUEUED' },
70
- 'reconcile.started': { target: 'RUNNING' },
71
- 'reconcile.completed': { target: 'SUCCEEDED' },
62
+ retry: { target: 'QUEUED', actions: CLEAR },
63
+ 'reconcile.started': { target: 'RUNNING', actions: CLEAR },
64
+ 'reconcile.completed': { target: 'SUCCEEDED', actions: CLEAR },
72
65
  'reconcile.blocked': { target: 'BLOCKED' },
73
66
  cancel: { target: 'CANCELLED' },
74
67
  },
75
68
  CANCELLED: {},
76
69
  };
70
+ const CANCEL_EFFECT = ['effect.cancelActiveTasks'];
71
+ const MERGE_EFFECT = ['effect.mergePullRequest'];
77
72
  const jobLegacy = {
78
73
  DRAFT: {
79
74
  activate: { target: 'ACTIVE' },
80
- 'pr.merged': { target: 'COMPLETED' },
75
+ 'pr.merged': { target: 'COMPLETED', actions: CANCEL_EFFECT },
81
76
  cancel: { target: 'CANCELLED' },
82
77
  },
83
78
  ACTIVE: {
84
- 'pr.merged': { target: 'COMPLETED' },
79
+ 'pr.merged': { target: 'COMPLETED', actions: CANCEL_EFFECT },
85
80
  cancel: { target: 'CANCELLED' },
86
81
  },
87
- COMPLETED: {},
82
+ COMPLETED: {
83
+ 'effect.cancelActiveTasks.failed': { target: 'COMPLETED' },
84
+ },
88
85
  CANCELLED: {
89
- 'pr.merged': { target: 'COMPLETED' },
86
+ 'pr.merged': { target: 'COMPLETED', actions: CANCEL_EFFECT },
87
+ 'effect.cancelActiveTasks.failed': { target: 'CANCELLED' },
90
88
  },
91
89
  };
92
- function taskLegacyDecision(from, event, context) {
90
+ function taskLegacyDecision(from, event, facts) {
93
91
  if (event === 'reconcile') {
94
- if (from === 'DRAFT' && context.dependenciesSatisfied === true)
95
- return { accepted: true, target: 'QUEUED', guard: 'dependenciesSatisfied', actions: ['release'], reason: 'accepted' };
96
- if (from === 'QUEUED' && context.dependenciesUnsatisfied === true)
97
- return { accepted: true, target: 'DRAFT', guard: 'dependenciesUnsatisfied', actions: ['hold'], reason: 'accepted' };
98
- if (from === 'QUEUED' && context.runnable === true)
99
- return { accepted: true, target: 'QUEUED', guard: 'runnable', actions: ['release'], reason: 'accepted' };
100
- return { accepted: false, target: null, guard: null, actions: [], reason: 'no_transition' };
92
+ if (from === 'DRAFT' && facts.dependenciesSatisfied === true)
93
+ return accepted('QUEUED', 'dependenciesSatisfied', CLEAR);
94
+ if (from === 'QUEUED' && facts.dependenciesUnsatisfied === true)
95
+ return accepted('DRAFT', 'dependenciesUnsatisfied');
96
+ if (from === 'QUEUED' && facts.runnable === true)
97
+ return accepted('QUEUED', 'runnable', CLEAR);
98
+ return emptyWorkflowDecision('no_transition');
99
+ }
100
+ if (event === 'dispatch') {
101
+ if (from === 'QUEUED' && facts.dispatchable === true)
102
+ return accepted('QUEUED', 'dispatchable', ['effect.launchTask']);
103
+ return emptyWorkflowDecision('no_transition');
101
104
  }
102
105
  const transition = taskLegacy[from][event];
103
106
  if (!transition)
104
- return { accepted: false, target: null, guard: null, actions: [], reason: 'no_transition' };
105
- return { accepted: true, target: transition.target, guard: transition.guard ?? null, actions: transition.actions ?? [], reason: 'accepted' };
107
+ return emptyWorkflowDecision('no_transition');
108
+ return accepted(transition.target, transition.guard ?? null, transition.actions ?? []);
106
109
  }
107
- function jobLegacyDecision(from, event, context) {
110
+ function jobLegacyDecision(from, event, facts) {
108
111
  if (event === 'pr.closed') {
109
- if ((from === 'DRAFT' || from === 'ACTIVE') && context.notMerged === true) {
110
- return { accepted: true, target: 'CANCELLED', guard: 'notMerged', actions: ['cancelJob'], reason: 'accepted' };
112
+ if ((from === 'DRAFT' || from === 'ACTIVE') && facts.notMerged === true) {
113
+ return accepted('CANCELLED', 'notMerged', CANCEL_EFFECT);
111
114
  }
112
- return { accepted: false, target: null, guard: null, actions: [], reason: 'no_transition' };
115
+ return emptyWorkflowDecision('no_transition');
116
+ }
117
+ if (event === 'review.approved' || event === 'implementation.completed') {
118
+ if (from === 'ACTIVE' && facts.autoMerge === true)
119
+ return accepted('ACTIVE', 'autoMerge', MERGE_EFFECT);
120
+ return emptyWorkflowDecision('no_transition');
113
121
  }
122
+ if (event === 'effect.mergePullRequest.failed' && from === 'ACTIVE')
123
+ return accepted('ACTIVE');
114
124
  const transition = jobLegacy[from][event];
115
125
  if (!transition)
116
- return { accepted: false, target: null, guard: null, actions: [], reason: 'no_transition' };
117
- return { accepted: true, target: transition.target, guard: transition.guard ?? null, actions: transition.actions ?? [], reason: 'accepted' };
126
+ return emptyWorkflowDecision('no_transition');
127
+ return accepted(transition.target, transition.guard ?? null, transition.actions ?? []);
118
128
  }
119
129
  function sameDecision(a, b) {
120
130
  return a.accepted === b.accepted
@@ -122,7 +132,11 @@ function sameDecision(a, b) {
122
132
  && a.guard === b.guard
123
133
  && a.reason === b.reason
124
134
  && a.actions.length === b.actions.length
125
- && a.actions.every((value, index) => value === b.actions[index]);
135
+ && a.actions.every((value, index) => value === b.actions[index])
136
+ && a.mutations.length === b.mutations.length
137
+ && a.mutations.every((value, index) => value === b.mutations[index])
138
+ && a.effects.length === b.effects.length
139
+ && a.effects.every((value, index) => value === b.effects[index]);
126
140
  }
127
141
  function report(kind, jobId, taskId, event, from, facts, legacy, graph) {
128
142
  const match = sameDecision(legacy, graph);
@@ -145,26 +159,26 @@ function report(kind, jobId, taskId, event, from, facts, legacy, graph) {
145
159
  if (!match && !reporter)
146
160
  console.warn(JSON.stringify(diagnostic));
147
161
  }
148
- export function decideTaskLifecycle(task, event, context = {}) {
149
- const legacy = taskLegacyDecision(task.state, event, context);
162
+ export function decideTaskLifecycle(task, event, facts = {}) {
163
+ const legacy = taskLegacyDecision(task.state, event, facts);
150
164
  const mode = workflowRolloutMode();
151
165
  if (mode === 'legacy')
152
166
  return legacy;
153
- const graph = graphDecision(taskLifecycleWorkflow, task.state, event, context);
167
+ const graph = graphWorkflowDecision(taskLifecycleWorkflow, task.state, event, facts);
154
168
  if (mode === 'shadow') {
155
- report('task', task.jobId ?? 'unknown', task.id, event, task.state, context, legacy, graph);
169
+ report('task', task.jobId ?? 'unknown', task.id, event, task.state, facts, legacy, graph);
156
170
  return legacy;
157
171
  }
158
172
  return graph;
159
173
  }
160
- export function decideJobLifecycle(job, event, context = {}) {
161
- const legacy = jobLegacyDecision(job.state, event, context);
174
+ export function decideJobLifecycle(job, event, facts = {}) {
175
+ const legacy = jobLegacyDecision(job.state, event, facts);
162
176
  const mode = workflowRolloutMode();
163
177
  if (mode === 'legacy')
164
178
  return legacy;
165
- const graph = graphDecision(jobLifecycleWorkflow, job.state, event, context);
179
+ const graph = graphWorkflowDecision(jobLifecycleWorkflow, job.state, event, facts);
166
180
  if (mode === 'shadow') {
167
- report('job', job.id, undefined, event, job.state, context, legacy, graph);
181
+ report('job', job.id, undefined, event, job.state, facts, legacy, graph);
168
182
  return legacy;
169
183
  }
170
184
  return graph;
@@ -0,0 +1,39 @@
1
+ import { classifyWorkflowActions, emptyWorkflowDecision, } from './contracts.js';
2
+ export function graphWorkflowDecision(engine, from, event, facts = {}) {
3
+ const enabled = engine.enabledSync(from, event, facts);
4
+ if (enabled.length === 0)
5
+ return emptyWorkflowDecision('no_transition');
6
+ if (enabled.length > 1)
7
+ return emptyWorkflowDecision('ambiguous');
8
+ const transition = enabled[0];
9
+ const actions = [...transition.actions];
10
+ const classified = classifyWorkflowActions(actions);
11
+ return {
12
+ accepted: true,
13
+ target: transition.to,
14
+ guard: transition.guard,
15
+ actions,
16
+ ...classified,
17
+ reason: 'accepted',
18
+ };
19
+ }
20
+ function applyTaskMutation(task, mutation) {
21
+ if (mutation === 'mutation.clearError')
22
+ task.error = null;
23
+ if (mutation === 'mutation.clearFailureReason')
24
+ task.failureReason = undefined;
25
+ }
26
+ export function applyTaskWorkflowDecision(task, decision, timestamp = new Date().toISOString()) {
27
+ if (!decision.accepted || !decision.target)
28
+ throw new Error(`Cannot apply rejected workflow decision to task ${task.id}`);
29
+ task.state = decision.target;
30
+ for (const mutation of decision.mutations)
31
+ applyTaskMutation(task, mutation);
32
+ task.updatedAt = timestamp;
33
+ }
34
+ export function applyJobWorkflowDecision(job, decision, timestamp = new Date().toISOString()) {
35
+ if (!decision.accepted || !decision.target)
36
+ throw new Error(`Cannot apply rejected workflow decision to job ${job.id}`);
37
+ job.state = decision.target;
38
+ job.updatedAt = timestamp;
39
+ }
@@ -0,0 +1,48 @@
1
+ import { decideTaskLifecycle } from './rollout.js';
2
+ import { applyTaskWorkflowDecision } from './runtime.js';
3
+ export function dependencyReady(task, tasks) {
4
+ return task.dependencies.every(id => tasks.get(id)?.state === 'SUCCEEDED');
5
+ }
6
+ export function detectCycles(tasks) {
7
+ const map = new Map(tasks.map(task => [task.id, task]));
8
+ const visiting = new Set();
9
+ const done = new Set();
10
+ const visit = (id) => {
11
+ if (visiting.has(id))
12
+ throw new Error(`Dependency cycle at ${id}`);
13
+ if (done.has(id))
14
+ return;
15
+ visiting.add(id);
16
+ for (const dependency of map.get(id)?.dependencies ?? [])
17
+ visit(dependency);
18
+ visiting.delete(id);
19
+ done.add(id);
20
+ };
21
+ for (const task of tasks)
22
+ visit(task.id);
23
+ }
24
+ function scheduledCycleTasks(job) {
25
+ const slot = job.schedule?.currentRunSlot ? Date.parse(job.schedule.currentRunSlot) : Number.NaN;
26
+ if (!Number.isFinite(slot))
27
+ return job.tasks;
28
+ return job.tasks.filter(task => Date.parse(task.createdAt) >= slot);
29
+ }
30
+ export function reconcileTaskDependencies(job) {
31
+ detectCycles(job.tasks);
32
+ const scopedTasks = scheduledCycleTasks(job);
33
+ const tasks = new Map(job.tasks.map(task => [task.id, task]));
34
+ for (const task of scopedTasks) {
35
+ const ready = dependencyReady(task, tasks);
36
+ const facts = {
37
+ dependenciesSatisfied: task.dependencies.length > 0 && ready,
38
+ dependenciesUnsatisfied: task.dependencies.length > 0 && !ready,
39
+ runnable: ready,
40
+ };
41
+ const decision = decideTaskLifecycle(task, 'reconcile', facts);
42
+ if (decision.reason === 'ambiguous')
43
+ throw new Error(`Ambiguous automatic task transition from ${task.state}`);
44
+ if (decision.accepted && decision.target)
45
+ applyTaskWorkflowDecision(task, decision);
46
+ }
47
+ return job;
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.90",
3
+ "version": "1.0.92",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,14 +2,19 @@ stateDiagram-v2
2
2
  [*] --> DRAFT
3
3
 
4
4
  DRAFT --> ACTIVE: activate
5
- DRAFT --> COMPLETED: pr.merged
5
+ DRAFT --> COMPLETED: pr.merged / effect.cancelActiveTasks
6
6
  DRAFT --> CANCELLED: cancel
7
- DRAFT --> CANCELLED: pr.closed [notMerged] / cancelJob
7
+ DRAFT --> CANCELLED: pr.closed [notMerged] / effect.cancelActiveTasks
8
8
 
9
- ACTIVE --> COMPLETED: pr.merged
9
+ ACTIVE --> ACTIVE: review.approved [autoMerge] / effect.mergePullRequest
10
+ ACTIVE --> ACTIVE: implementation.completed [autoMerge] / effect.mergePullRequest
11
+ ACTIVE --> ACTIVE: effect.mergePullRequest.failed
12
+ ACTIVE --> COMPLETED: pr.merged / effect.cancelActiveTasks
10
13
  ACTIVE --> CANCELLED: cancel
11
- ACTIVE --> CANCELLED: pr.closed [notMerged] / cancelJob
14
+ ACTIVE --> CANCELLED: pr.closed [notMerged] / effect.cancelActiveTasks
12
15
 
13
- CANCELLED --> COMPLETED: pr.merged
16
+ CANCELLED --> COMPLETED: pr.merged / effect.cancelActiveTasks
17
+ CANCELLED --> CANCELLED: effect.cancelActiveTasks.failed
18
+ COMPLETED --> COMPLETED: effect.cancelActiveTasks.failed
14
19
 
15
20
  COMPLETED --> [*]
@@ -1,30 +1,32 @@
1
1
  stateDiagram-v2
2
2
  [*] --> DRAFT
3
3
 
4
- DRAFT --> QUEUED: release
5
- DRAFT --> QUEUED: reconcile [dependenciesSatisfied] / release
4
+ DRAFT --> QUEUED: release / mutation.clearError,mutation.clearFailureReason
5
+ DRAFT --> QUEUED: reconcile [dependenciesSatisfied] / mutation.clearError,mutation.clearFailureReason
6
6
  DRAFT --> BLOCKED: block
7
7
  DRAFT --> CANCELLED: cancel
8
8
 
9
9
  QUEUED --> DRAFT: hold
10
- QUEUED --> DRAFT: reconcile [dependenciesUnsatisfied] / hold
11
- QUEUED --> QUEUED: reconcile [runnable] / release
12
- QUEUED --> RUNNING: task.started
10
+ QUEUED --> DRAFT: reconcile [dependenciesUnsatisfied]
11
+ QUEUED --> QUEUED: reconcile [runnable] / mutation.clearError,mutation.clearFailureReason
12
+ QUEUED --> QUEUED: dispatch [dispatchable] / effect.launchTask
13
+ QUEUED --> RUNNING: task.started / mutation.clearError,mutation.clearFailureReason
13
14
  QUEUED --> BLOCKED: block
14
15
  QUEUED --> FAILED: fail
16
+ QUEUED --> FAILED: effect.launchTask.failed
15
17
  QUEUED --> CANCELLED: cancel
16
18
 
17
19
  RUNNING --> BLOCKED: task.blocked
18
- RUNNING --> SUCCEEDED: task.completed
20
+ RUNNING --> SUCCEEDED: task.completed / mutation.clearError,mutation.clearFailureReason
19
21
  RUNNING --> FAILED: task.failed
20
22
  RUNNING --> CANCELLED: task.cancelled
21
23
 
22
- BLOCKED --> QUEUED: retry
24
+ BLOCKED --> QUEUED: retry / mutation.clearError,mutation.clearFailureReason
23
25
  BLOCKED --> CANCELLED: cancel
24
26
 
25
- FAILED --> QUEUED: retry
26
- FAILED --> RUNNING: reconcile.started
27
- FAILED --> SUCCEEDED: reconcile.completed
27
+ FAILED --> QUEUED: retry / mutation.clearError,mutation.clearFailureReason
28
+ FAILED --> RUNNING: reconcile.started / mutation.clearError,mutation.clearFailureReason
29
+ FAILED --> SUCCEEDED: reconcile.completed / mutation.clearError,mutation.clearFailureReason
28
30
  FAILED --> BLOCKED: reconcile.blocked
29
31
  FAILED --> CANCELLED: cancel
30
32