agents-relay 2.0.101 → 2.0.103
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 +3 -3
- package/dist/cli-help.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/dashboard-actions.js +1 -5
- package/dist/dashboard-html.js +4 -4
- package/dist/interactive-resume.js +1 -1
- package/dist/local-authority-store.js +1 -1
- package/dist/planner-runtime.js +22 -1
- package/dist/planner.js +21 -14
- package/dist/projected-store.js +1 -1
- package/dist/reconciler-cancellation.js +1 -1
- package/dist/relay-store.js +8 -2
- package/dist/runtime-status.js +1 -1
- package/dist/store.js +2 -1
- package/dist/task-actions.js +11 -4
- package/dist/task-update.js +1 -1
- package/dist/troubleshooter.js +1 -1
- package/dist/types.js +11 -0
- package/dist/unblock.js +7 -7
- package/dist/workflow/definitions-v2.js +2 -1
- package/dist/workflow/planner-orchestration-v2.js +2 -2
- package/dist/workflow/task-reconcile.js +2 -0
- package/package.json +1 -1
- package/workflow/task-lifecycle.mmd +9 -5
package/dist/blockers.js
CHANGED
|
@@ -98,16 +98,16 @@ export async function resolveBlockerDependencies(options) {
|
|
|
98
98
|
catch (error) {
|
|
99
99
|
loadError = error instanceof Error ? error.message : String(error);
|
|
100
100
|
}
|
|
101
|
-
const targetMatches = !!task && ['DRAFT', 'FAILED', 'BLOCKED', '
|
|
101
|
+
const targetMatches = !!task && ['DRAFT', 'FAILED', 'BLOCKED', 'QUEUED'].includes(task.state) && task.attempt === relation.blockedAttempt;
|
|
102
102
|
let note = 'dependent task requeued';
|
|
103
103
|
const decision = await dispatchBlockerResolution({
|
|
104
104
|
resolutionAllowed: !(mode === 'verified' && options.resolution !== 'verified'),
|
|
105
105
|
targetMatches,
|
|
106
106
|
autonomousJob: targetJob?.executionMode === 'autonomous',
|
|
107
107
|
effectivelyBlocked: !!task && isEffectivelyBlocked(task),
|
|
108
|
-
alreadyUnblocked: task?.
|
|
108
|
+
alreadyUnblocked: Boolean(task?.unblock && ['DRAFT', 'QUEUED'].includes(task.state)),
|
|
109
109
|
isDraft: task?.state === 'DRAFT', isBlocked: task?.state === 'BLOCKED',
|
|
110
|
-
isFailed: task?.state === 'FAILED', isUnblocked: task?.
|
|
110
|
+
isFailed: task?.state === 'FAILED', isUnblocked: Boolean(task?.unblock && ['DRAFT', 'QUEUED'].includes(task.state)),
|
|
111
111
|
}, async (actionId) => {
|
|
112
112
|
if (!targetJob || !task)
|
|
113
113
|
throw new Error('Blocker graph requested action without a matching dependent task');
|
package/dist/cli-help.js
CHANGED
|
@@ -90,7 +90,7 @@ Example:
|
|
|
90
90
|
'job merge': `Request an idempotent external pull-request merge; return QUEUED until confirmed, when the Graph Engine completes the Job.
|
|
91
91
|
|
|
92
92
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. The current PR head SHA is pinned into the merge request.`,
|
|
93
|
-
'job unblock': `Authoritatively unblock the effective BLOCKED work Tasks in one Job.\n\nRequired: --repo OWNER/REPO --pr NUMBER --id JOB_ID --reason TEXT. Autonomous Jobs
|
|
93
|
+
'job unblock': `Authoritatively unblock the effective BLOCKED work Tasks in one Job.\n\nRequired: --repo OWNER/REPO --pr NUMBER --id JOB_ID --reason TEXT. Autonomous Jobs move blocked work to planner-waiting DRAFT with durable unblock facts; fixed Jobs resume directly as QUEUED with a reset retry budget.`,
|
|
94
94
|
'job cancel': `Cancel the durable Job and close its backing pull request.
|
|
95
95
|
|
|
96
96
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. Nonterminal Tasks are cancelled before the PR is closed.`,
|
package/dist/cli.js
CHANGED
|
@@ -153,7 +153,7 @@ async function runTaskCommand(action, args) {
|
|
|
153
153
|
if (!task)
|
|
154
154
|
throw new Error(`Task ${taskId} not found`);
|
|
155
155
|
const expectedUpdatedAt = task.updatedAt;
|
|
156
|
-
if (['RUNNING', '
|
|
156
|
+
if (['RUNNING', 'SUCCEEDED', 'CANCELLED'].includes(task.state))
|
|
157
157
|
throw new Error(`Task ${taskId} is ${task.state}; durable execution fields are immutable in this state`);
|
|
158
158
|
if (hasArg(args, '--title'))
|
|
159
159
|
task.title = arg(args, '--title');
|
|
@@ -260,7 +260,7 @@ async function runTaskCommand(action, args) {
|
|
|
260
260
|
return;
|
|
261
261
|
}
|
|
262
262
|
const expectedUpdatedAt = task.updatedAt;
|
|
263
|
-
if (['RUNNING', '
|
|
263
|
+
if (['RUNNING', 'SUCCEEDED', 'CANCELLED'].includes(task.state))
|
|
264
264
|
throw new Error(`Task ${id} is ${task.state}; durable execution fields are immutable in this state`);
|
|
265
265
|
if (hasArg(args, '--adapter')) {
|
|
266
266
|
const adapter = arg(args, '--adapter');
|
|
@@ -46,11 +46,7 @@ export function dashboardTaskSetState(store) {
|
|
|
46
46
|
if (task.state === state)
|
|
47
47
|
return job;
|
|
48
48
|
if (task.state === 'BLOCKED' && state !== 'CANCELLED')
|
|
49
|
-
throw new Error(`Task ${task.id} is BLOCKED; only authoritative unblock may
|
|
50
|
-
if (state === 'UNBLOCKED')
|
|
51
|
-
throw new Error(`Task ${task.id} may enter UNBLOCKED only through authoritative unblock`);
|
|
52
|
-
if (task.state === 'UNBLOCKED')
|
|
53
|
-
throw new Error(`Task ${task.id} is historical UNBLOCKED work and cannot be changed by dashboard state override`);
|
|
49
|
+
throw new Error(`Task ${task.id} is BLOCKED; only authoritative unblock may resume it`);
|
|
54
50
|
const now = new Date().toISOString();
|
|
55
51
|
const from = task.state;
|
|
56
52
|
await dispatchWorkflowTargetOverride(taskLifecycleV2, {
|
package/dist/dashboard-html.js
CHANGED
|
@@ -61,7 +61,7 @@ export const dashboardHtml = `<!doctype html>
|
|
|
61
61
|
.status-pill.ok,.badge.ACTIVE,.badge.RUNNING,.badge.SUCCEEDED,.badge.COMPLETED,.badge.MERGED { background:#e5f3e9; color:var(--ok); }
|
|
62
62
|
.status-pill.warn,.badge.BLOCKED,.badge.DRAFT,.badge.QUEUED,.badge.WAITING,.badge.OPEN { background:#fff1d2; color:var(--warn); }
|
|
63
63
|
.status-pill.bad,.badge.FAILED { background:#ffe8e8; color:var(--bad); }
|
|
64
|
-
.status-pill.neutral,.badge.IDLE,.badge.
|
|
64
|
+
.status-pill.neutral,.badge.IDLE,.badge.CANCELLED { background:#edf0f4; color:var(--muted); }
|
|
65
65
|
.badge.DRAFT { background:#eef2ff; color:#3f51b5; }
|
|
66
66
|
.badge.MERGE_PENDING { background:#ffe3a3; color:#7a4a00; }
|
|
67
67
|
.job-summary,.detail-groups { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.55rem; }
|
|
@@ -339,7 +339,7 @@ function shortManagedId(id,repository,kind){const parts=String(repository||'').s
|
|
|
339
339
|
function jobForSelection(selection){return jobs.find(job=>jobSelectionKey(job)===selection)||jobs.find(job=>job.id===selection)||null;}
|
|
340
340
|
function selected(){return jobForSelection(selectedJob)||jobs[0]||null;}
|
|
341
341
|
function counts(tasks){const out={};for(const task of Array.isArray(tasks)?tasks:[])out[task.state]=(out[task.state]||0)+1;return out;}
|
|
342
|
-
function compactCounts(input){const order=['RUNNING','BLOCKED','
|
|
342
|
+
function compactCounts(input){const order=['RUNNING','BLOCKED','FAILED','QUEUED','DRAFT','SUCCEEDED','CANCELLED'];return order.filter(state=>input[state]).map(state=>input[state]+' '+state).join(', ')||'No tasks';}
|
|
343
343
|
function relativeTime(timestamp){if(!timestamp)return '—';const parsed=Date.parse(timestamp);if(!Number.isFinite(parsed))return value(timestamp);const seconds=Math.max(0,Math.round((Date.now()-parsed)/1000));if(seconds<60)return 'just now';if(seconds<3600)return Math.floor(seconds/60)+'m ago';if(seconds<86400)return Math.floor(seconds/3600)+'h ago';return Math.floor(seconds/86400)+'d ago';}
|
|
344
344
|
function cadence(job){const schedule=job?.schedule;if(!schedule?.cron)return '—';const cron=String(schedule.cron).trim();const timezone=value(schedule.timezone||'UTC');const fields=cron.split(/\\s+/);let label=cron;if(fields.length===5){const [minute,hour,day,month,weekday]=fields;if(day==='*'&&month==='*'&&weekday==='*'){const everyMinute=minute.match(/^\\*\\/(\\d+)$/);if(everyMinute&&hour==='*')label='Every '+everyMinute[1]+' min';else if(minute==='0'&&hour==='*')label='Hourly';else if(/^\\d+$/.test(minute)&&/^\\d+$/.test(hour)){const hh=String(Number(hour)).padStart(2,'0');const mm=String(Number(minute)).padStart(2,'0');label='Daily '+hh+':'+mm;}}}return label+' · '+cron+' · '+timezone;}
|
|
345
345
|
function safePrUrl(repository,prNumber){if(typeof repository!=='string'||!Number.isInteger(prNumber)||prNumber<=0)return null;const parts=repository.split('/');if(parts.length!==2)return null;const [owner,repo]=parts;if(!owner||!repo||owner==='.'||owner==='..'||repo==='.'||repo==='..')return null;if(!/^[A-Za-z0-9-]+$/.test(owner)||!/^[A-Za-z0-9._-]+$/.test(repo))return null;return 'https://github.com/'+owner+'/'+repo+'/pull/'+prNumber;}
|
|
@@ -397,9 +397,9 @@ function attemptBudget(task){const attempt=Number(task?.attempt)||0;const max=Nu
|
|
|
397
397
|
const JOB_STATES=['DRAFT','ACTIVE','COMPLETED','CANCELLED'];
|
|
398
398
|
async function setJobState(job,state,select){if(!job||state===job.state)return;select.disabled=true;try{const params=new URLSearchParams({id:job.id,state,repo:job.repository,pr:String(job.prNumber)});const response=await fetch('/api/job-state?'+params.toString(),{method:'POST'});if(!response.ok)throw new Error(response.status+' '+response.statusText);const updated=await response.json();const index=jobs.findIndex(candidate=>jobSelectionKey(candidate)===jobSelectionKey(job));if(index>=0)jobs[index]=updated;else jobs.push(updated);renderAll();void loadEventHistory();}catch(error){select.title='Set job lifecycle state: '+error.message;select.value=job.state;}finally{select.disabled=false;}}
|
|
399
399
|
function jobStateField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('Lifecycle state')));const select=document.createElement('select');select.className='job-filter job-state-select';select.setAttribute('aria-label','Job lifecycle state');for(const state of JOB_STATES)select.append(new Option(state,state));select.value=job.state;select.addEventListener('change',()=>void setJobState(job,select.value,select));box.append(select);return box;}
|
|
400
|
-
const TASK_STATES=['DRAFT','QUEUED','RUNNING','BLOCKED','
|
|
400
|
+
const TASK_STATES=['DRAFT','QUEUED','RUNNING','BLOCKED','SUCCEEDED','FAILED','CANCELLED'];
|
|
401
401
|
async function setTaskState(task,state,select){const job=selected();if(!job||state===task.state)return;select.disabled=true;try{const params=new URLSearchParams({id:job.id,task:task.id,state,repo:job.repository,pr:String(job.prNumber)});const response=await fetch('/api/task-state?'+params.toString(),{method:'POST'});if(!response.ok)throw new Error(response.status+' '+response.statusText);const updated=await response.json();const index=jobs.findIndex(candidate=>jobSelectionKey(candidate)===jobSelectionKey(job));if(index>=0)jobs[index]=updated;else jobs.push(updated);renderAll();void loadEventHistory();}catch(error){select.title='Set task state: '+error.message;select.value=task.state;}finally{select.disabled=false;}}
|
|
402
|
-
function taskStateField(task){const box=element('div','detail-group');box.append(element('p','detail-label',text('State')));const select=document.createElement('select');select.className='job-filter task-state-select';select.setAttribute('aria-label','Task state');for(const state of TASK_STATES)
|
|
402
|
+
function taskStateField(task){const box=element('div','detail-group');box.append(element('p','detail-label',text('State')));const select=document.createElement('select');select.className='job-filter task-state-select';select.setAttribute('aria-label','Task state');for(const state of TASK_STATES)select.append(new Option(state,state));select.value=task.state;select.addEventListener('change',()=>void setTaskState(task,select.value,select));box.append(select);return box;}
|
|
403
403
|
function manualOverrideField(task){const rows=Array.isArray(task.manualStateOverrides)?task.manualStateOverrides:[];const last=rows.at(-1);return field('Manual override',last?last.from+' → '+last.to+' · '+relativeTime(last.at):'—');}
|
|
404
404
|
function renderTaskDetail(task){taskDetailRoot.replaceChildren();if(!task){taskDetailRoot.append(element('p','empty',text('Select a task to inspect execution details.')));return;}const routing=task.routing||{};const groups=element('div','detail-groups');groups.append(field('Title',task.title||'—'),field('Task ID',task.id),field('Kind',task.kind||'work'),taskStateField(task),manualOverrideField(task),field('Planning decision',task.planningDecisionId||'—'),field('Planning source',task.planningSource||'—'),field('Planning round',task.planningRound===undefined?'—':String(task.planningRound)),field('Project / agent',value(task.projectName)+' / '+value(task.agentName)),field('Parent task',task.parentTaskId||'root'),field('Adapter',task.adapter),field('Provider / model',value(routing.provider)+' / '+value(routing.model)),field('Reasoning / profile',value(routing.reasoning||routing.profile)),field('Attempt',attemptBudget(task)),field('Dependencies',Array.isArray(task.dependencies)&&task.dependencies.length?task.dependencies.join(', '):'none'),taskCommentField(task),chatField(task));const job=selected();if(task.reviewHeadSha&&job)groups.append(commitField('HEAD commit',job,task.reviewHeadSha));const graph=agentGraphField(task);if(graph)groups.append(graph);if(task.plannerResult)groups.append(field('Objective status',task.plannerResult.objective_status),markdownField('Assessment',task.plannerResult.assessment));if(task.error)groups.append(field('Error',task.error));taskDetailRoot.append(groups);}
|
|
405
405
|
function reviewStatus(job){const review=job?.review;if(!review)return 'Not started';if(review.outcome==='approved'&&review.headSha===job.headSha)return 'MERGE READY · approved';const stale=Array.isArray(job?.tasks)&&job.tasks.some(task=>task.agentName==='reviewer'&&task.reviewHeadSha&&job.headSha&&task.reviewHeadSha!==job.headSha);if(stale)return 'STALE · awaiting review';return String(review.outcome||'pending').replace('_',' ').toUpperCase();}
|
|
@@ -13,7 +13,7 @@ export async function resumeInteractiveTask(store, jobId, taskId, invocationId,
|
|
|
13
13
|
invocation.status !== decision || !invocation.decidedAt)
|
|
14
14
|
return job;
|
|
15
15
|
// Both state transitions and retry eligibility are owned by task-lifecycle.mmd.
|
|
16
|
-
if (!tryTransitionTaskEvent(task, 'task.unblocked').accepted) {
|
|
16
|
+
if (!tryTransitionTaskEvent(task, 'task.unblocked', { autonomousJob: false }).accepted) {
|
|
17
17
|
throw new Error('Interactive Task unblock rejected by canonical lifecycle Graph');
|
|
18
18
|
}
|
|
19
19
|
applyTaskUnblockRetrySnapshot(task);
|
|
@@ -14,7 +14,7 @@ import { nextScheduleRun, scheduleBucket } from './scheduler.js';
|
|
|
14
14
|
* not lifecycle storage. An explicit one-time import must use RelayStore directly.
|
|
15
15
|
*/
|
|
16
16
|
const TASK_STATE_ORDER = {
|
|
17
|
-
DRAFT: 0, QUEUED: 1,
|
|
17
|
+
DRAFT: 0, QUEUED: 1, RUNNING: 2, BLOCKED: 3,
|
|
18
18
|
SUCCEEDED: 5, FAILED: 5, CANCELLED: 5,
|
|
19
19
|
};
|
|
20
20
|
export class LocalAuthorityDurableStore {
|
package/dist/planner-runtime.js
CHANGED
|
@@ -8,6 +8,7 @@ import { samePlannerChildDefinition } from './planner-child.js';
|
|
|
8
8
|
import { dispatchJobLifecycleSync } from './workflow/lifecycle.js';
|
|
9
9
|
import { withExecutionBoundary } from './reconciler-runtime.js';
|
|
10
10
|
import { storySizeTimeoutMs } from './types.js';
|
|
11
|
+
import { tryTransitionTaskEvent } from './scheduler.js';
|
|
11
12
|
/** Activate queued planner work inside the same authoritative SQLite mutation. */
|
|
12
13
|
export function activateCommittedPlannerWork(job) {
|
|
13
14
|
if (job.state !== 'DRAFT' || !job.tasks.some(item => item.state === 'QUEUED'))
|
|
@@ -80,7 +81,7 @@ async function finishPlanner(ports, jobId, planId, executionId, error, raw) {
|
|
|
80
81
|
return;
|
|
81
82
|
const attempt = fresh.attempt ?? 0;
|
|
82
83
|
const maxAttempts = fresh.maxAttempts ?? (fresh.manualPlanning ? 1 : 3);
|
|
83
|
-
retry = attempt < maxAttempts;
|
|
84
|
+
retry = attempt < maxAttempts && !message.startsWith('Planner JSON correction failed after one retry:');
|
|
84
85
|
fresh.status = retry ? 'PLANNING' : 'FAILED';
|
|
85
86
|
fresh.retryFeedback = message;
|
|
86
87
|
fresh.error = message;
|
|
@@ -167,6 +168,17 @@ async function finishPlanner(ports, jobId, planId, executionId, error, raw) {
|
|
|
167
168
|
else if (!samePlannerChildDefinition(existing, child))
|
|
168
169
|
throw new Error(`Planner child ${child.id} already exists with a different definition`);
|
|
169
170
|
}
|
|
171
|
+
const consumedUnblocks = new Set(durablePlan.trigger.source === 'unblock' ? durablePlan.trigger.taskIds ?? [] : []);
|
|
172
|
+
for (const task of current.tasks) {
|
|
173
|
+
if (!consumedUnblocks.has(task.id) || !task.unblock || task.state === 'CANCELLED' || durablePlan.generatedTaskIds.includes(task.id))
|
|
174
|
+
continue;
|
|
175
|
+
if (tryTransitionTaskEvent(task, 'cancel').accepted) {
|
|
176
|
+
task.error = `Superseded by successful unblock plan ${durablePlan.id}`;
|
|
177
|
+
task.leaseOwner = null;
|
|
178
|
+
task.leaseExpiresAt = null;
|
|
179
|
+
task.executionId = null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
170
182
|
// Planner children are committed atomically with Job activation.
|
|
171
183
|
// Otherwise the DRAFT Job is invisible to the ACTIVE-only dispatcher.
|
|
172
184
|
activateCommittedPlannerWork(current);
|
|
@@ -199,6 +211,15 @@ async function finishPlanner(ports, jobId, planId, executionId, error, raw) {
|
|
|
199
211
|
job.updatedAt = now;
|
|
200
212
|
await ports.store.saveJob(job);
|
|
201
213
|
}
|
|
214
|
+
const consumedUnblocks = new Set(plan.trigger.source === 'unblock' ? plan.trigger.taskIds ?? [] : []);
|
|
215
|
+
for (const task of job.tasks) {
|
|
216
|
+
if (!consumedUnblocks.has(task.id) || !task.unblock || task.state === 'CANCELLED' || plan.generatedTaskIds.includes(task.id))
|
|
217
|
+
continue;
|
|
218
|
+
if (tryTransitionTaskEvent(task, 'cancel').accepted) {
|
|
219
|
+
task.error = `Superseded by successful unblock plan ${plan.id}`;
|
|
220
|
+
await ports.store.saveTask(task);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
202
223
|
for (const child of children)
|
|
203
224
|
if (!job.tasks.some(item => item.id === child.id)) {
|
|
204
225
|
const spec = result.next_tasks.find(item => item.id === child.id);
|
package/dist/planner.js
CHANGED
|
@@ -21,8 +21,8 @@ export function semanticTaskTree(tasks) {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
function plannerTrigger(job, manual = false) {
|
|
24
|
-
const consumed = new Set((job.plans ?? []).flatMap(plan => plan.trigger.source === 'unblock' ? plan.trigger.taskIds ?? [] : []));
|
|
25
|
-
const unblocked = job.tasks.filter(task =>
|
|
24
|
+
const consumed = new Set((job.plans ?? []).filter(plan => plan.status === 'SUCCEEDED').flatMap(plan => plan.trigger.source === 'unblock' ? plan.trigger.taskIds ?? [] : []));
|
|
25
|
+
const unblocked = job.tasks.filter(task => ['DRAFT', 'QUEUED', 'UNBLOCKED'].includes(task.state) && task.unblock && !consumed.has(task.id));
|
|
26
26
|
const hasUnblock = unblocked.length > 0 || Boolean(job.changeCycle && !job.changeCycle.consumedByPlannerTaskId);
|
|
27
27
|
const decision = dispatchPlannerTrigger({
|
|
28
28
|
manualPlan: manual,
|
|
@@ -502,17 +502,14 @@ export class AgentObjectivePlanner {
|
|
|
502
502
|
this.agentFile = agentFile;
|
|
503
503
|
}
|
|
504
504
|
async plan(context, signal) {
|
|
505
|
-
const resolvedPlanner = context.executionContext?.agents.find(agent => agent.name === 'planner')?.path;
|
|
506
505
|
const effectivePlanner = context.executionContext ? await resolveAgentMarkdown('planner', BUNDLED_AGENTS_ROOT, context.executionContext.project.path) : undefined;
|
|
507
506
|
const instructions = effectivePlanner?.content ?? await readFile(this.agentFile, 'utf8');
|
|
508
507
|
const contract = await readFile(DEFAULT_TASK_JOB_CONTRACT, 'utf8');
|
|
509
508
|
const resolvedContext = context.executionContext ? executionContextPrompt(context.executionContext) + '\n\n' : '';
|
|
510
509
|
const prompt = (effectivePlanner ? `[Effective planner contract SHA-256: ${effectivePlanner.hash}]\n\n` : '') + instructions.trim() + '\n\n## Shared Task / Job Contract\n\n' + contract.trim() + '\n\n' + resolvedContext + '## Durable planner input\n\n' + plannerInput(context);
|
|
511
|
-
|
|
512
|
-
const child = spawnOwnedProcess(this.command, ['exec', '--json', '--',
|
|
513
|
-
let stdoutBuffer = '';
|
|
514
|
-
let finalMessage = '';
|
|
515
|
-
let stderrTail = '';
|
|
510
|
+
const runAgent = (input) => new Promise((resolve, reject) => {
|
|
511
|
+
const child = spawnOwnedProcess(this.command, ['exec', '--json', '--', input], { invocation: 'planner.agent', signal, correlation: { jobId: context.job.id }, spawn: { cwd: context.executionContext?.project.path, stdio: ['ignore', 'pipe', 'pipe'] } });
|
|
512
|
+
let stdoutBuffer = '', finalMessage = '', stderrTail = '';
|
|
516
513
|
const consume = (line) => {
|
|
517
514
|
const message = parsePlannerAgentMessage(line);
|
|
518
515
|
if (message)
|
|
@@ -538,13 +535,23 @@ export class AgentObjectivePlanner {
|
|
|
538
535
|
reject(new Error('Planner agent returned no final message'));
|
|
539
536
|
return;
|
|
540
537
|
}
|
|
541
|
-
|
|
542
|
-
resolve(parsePlannerOutput(finalMessage));
|
|
543
|
-
}
|
|
544
|
-
catch (error) {
|
|
545
|
-
reject(error);
|
|
546
|
-
}
|
|
538
|
+
resolve(finalMessage);
|
|
547
539
|
});
|
|
548
540
|
});
|
|
541
|
+
const first = await runAgent(prompt);
|
|
542
|
+
try {
|
|
543
|
+
return parsePlannerOutput(first);
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
if (signal.aborted)
|
|
547
|
+
throw error;
|
|
548
|
+
const correction = `${prompt}\n\n## Format correction (one retry only)\nYour previous final response was not valid Planner JSON. Error: ${error instanceof Error ? error.message : String(error)}\nPrevious response (possibly truncated):\n${first.slice(0, 12000)}\nReturn exactly one valid JSON object conforming to the Planner contract. No prose, no second JSON object.`;
|
|
549
|
+
try {
|
|
550
|
+
return parsePlannerOutput(await runAgent(correction));
|
|
551
|
+
}
|
|
552
|
+
catch (retryError) {
|
|
553
|
+
throw new Error(`Planner JSON correction failed after one retry: ${retryError instanceof Error ? retryError.message : String(retryError)}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
549
556
|
}
|
|
550
557
|
}
|
package/dist/projected-store.js
CHANGED
|
@@ -344,7 +344,7 @@ export class ProjectedDurableStore {
|
|
|
344
344
|
tasks.push(structuredClone(task));
|
|
345
345
|
job.tasks = tasks;
|
|
346
346
|
const value = structuredClone(task);
|
|
347
|
-
const terminal = ['SUCCEEDED', 'FAILED', 'BLOCKED', '
|
|
347
|
+
const terminal = ['SUCCEEDED', 'FAILED', 'BLOCKED', 'CANCELLED'].includes(task.state);
|
|
348
348
|
await this.relay.saveJobWithProjection(job, {
|
|
349
349
|
idempotencyKey: `save_task:${task.jobId}:${task.id}`,
|
|
350
350
|
coalesceKey: taskKey(task),
|
|
@@ -12,7 +12,7 @@ export async function cancelActiveTasks(store, jobId, reason, cancelPlanner, can
|
|
|
12
12
|
plan.updatedAt = new Date().toISOString();
|
|
13
13
|
await store.savePlan(plan);
|
|
14
14
|
}
|
|
15
|
-
for (const task of job.tasks.filter(item => item.kind !== 'planner' && !['SUCCEEDED', 'FAILED', '
|
|
15
|
+
for (const task of job.tasks.filter(item => item.kind !== 'planner' && !['SUCCEEDED', 'FAILED', 'CANCELLED'].includes(item.state))) {
|
|
16
16
|
cancelExecutionGeneration(job.id, task.id, task.executionId ?? task.executionHistory?.at(-1)?.id);
|
|
17
17
|
cancelPlanner(task.id);
|
|
18
18
|
await applyTaskCancelAction(task, reason);
|
package/dist/relay-store.js
CHANGED
|
@@ -2,6 +2,7 @@ import { preserveTaskComments } from './task-comments.js';
|
|
|
2
2
|
import { DatabaseSync } from 'node:sqlite';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
import { mkdirSync } from 'node:fs';
|
|
5
|
+
import { normalizeLegacyTaskLifecycle } from './types.js';
|
|
5
6
|
import { recordPublicationInDatabase, loadPublicationFromDatabase, listPublicationEventsFromDatabase } from './publication-store.js';
|
|
6
7
|
import { observability } from './observability.js';
|
|
7
8
|
import { createJobIfAbsent } from './relay-create.js';
|
|
@@ -140,11 +141,15 @@ export class SQLiteRelayStore {
|
|
|
140
141
|
async loadJob(jobId) {
|
|
141
142
|
this.assertScope(jobId);
|
|
142
143
|
const row = this.db.prepare('SELECT json FROM job_snapshot WHERE job_id=?').get(jobId);
|
|
143
|
-
|
|
144
|
+
if (!row?.json)
|
|
145
|
+
return null;
|
|
146
|
+
const job = normalizeLegacyJobPrNumber(JSON.parse(row.json));
|
|
147
|
+
job.tasks = job.tasks.map(task => normalizeLegacyTaskLifecycle(task, job.executionMode));
|
|
148
|
+
return job;
|
|
144
149
|
}
|
|
145
150
|
async listJobs() {
|
|
146
151
|
const rows = this.db.prepare(this.scopeJobId ? 'SELECT json FROM job_snapshot WHERE job_id=? ORDER BY job_id' : 'SELECT json FROM job_snapshot ORDER BY job_id').all(...(this.scopeJobId ? [this.scopeJobId] : []));
|
|
147
|
-
return rows.map(row => normalizeLegacyJobPrNumber(JSON.parse(row.json)));
|
|
152
|
+
return rows.map(row => { const job = normalizeLegacyJobPrNumber(JSON.parse(row.json)); job.tasks = job.tasks.map(task => normalizeLegacyTaskLifecycle(task, job.executionMode)); return job; });
|
|
148
153
|
}
|
|
149
154
|
async saveJob(job) {
|
|
150
155
|
if (!this.scopeJobId) {
|
|
@@ -176,6 +181,7 @@ export class SQLiteRelayStore {
|
|
|
176
181
|
if (!row?.json)
|
|
177
182
|
throw new Error(`Local authoritative Job ${jobId} not found`);
|
|
178
183
|
const current = normalizeLegacyJobPrNumber(JSON.parse(row.json));
|
|
184
|
+
current.tasks = current.tasks.map(task => normalizeLegacyTaskLifecycle(task, current.executionMode));
|
|
179
185
|
const before = JSON.stringify(current);
|
|
180
186
|
const next = mutation(current);
|
|
181
187
|
if (next.id !== jobId)
|
package/dist/runtime-status.js
CHANGED
|
@@ -7,7 +7,7 @@ export function implementationTasks(job) {
|
|
|
7
7
|
&& task.agentName !== 'reviewer'
|
|
8
8
|
&& task.agentName !== 'troubleshooter'
|
|
9
9
|
&& !isSuperseded(task)
|
|
10
|
-
&& task.state !== '
|
|
10
|
+
&& task.state !== 'DRAFT');
|
|
11
11
|
}
|
|
12
12
|
export function implementationComplete(job) {
|
|
13
13
|
const tasks = implementationTasks(job);
|
package/dist/store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeLegacyTaskLifecycle } from './types.js';
|
|
1
2
|
import { parseAgent, parseJob, parsePlan, parseTask, renderAgentComment, renderJobComment, renderPlanComment, renderTaskComment, AGENT_MARKER, JOB_MARKER, PLAN_MARKER, TASK_MARKER } from './markers.js';
|
|
2
3
|
import { DurableDefinitionConflictError, isTaskForJob, planDefinitionDivergence, samePlanDefinition, sameTaskDefinition, taskDefinitionDivergence } from './store-task-definition.js';
|
|
3
4
|
import { parsePackagePublicationMarker } from './github-webhook.js';
|
|
@@ -91,7 +92,7 @@ export class InMemoryStore {
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
async load(jobId) { const job = await this.findJob(jobId); if (!job)
|
|
94
|
-
throw new Error(`Job ${jobId} not found`); job.plans = this.plansForJob(jobId); job.tasks = this.tasksForJob(jobId); return job; }
|
|
95
|
+
throw new Error(`Job ${jobId} not found`); job.plans = this.plansForJob(jobId); job.tasks = this.tasksForJob(jobId).map(task => normalizeLegacyTaskLifecycle(task, job.executionMode)); return job; }
|
|
95
96
|
async saveJob(job) { this.values.set(`${JOB_MARKER}:${job.id}`, renderJobComment(job)); }
|
|
96
97
|
async appendPlan(plan) { const key = `${PLAN_MARKER}:${plan.jobId}:${plan.id}`; const existing = this.values.get(key); if (existing) {
|
|
97
98
|
const current = parsePlan(existing);
|
package/dist/task-actions.js
CHANGED
|
@@ -40,9 +40,12 @@ function applyTaskRetrySnapshot(task, policy = {}) {
|
|
|
40
40
|
}
|
|
41
41
|
/** Fixed-job authoritative unblock continuation, applied inside one Job snapshot mutation. */
|
|
42
42
|
export function applyTaskUnblockRetrySnapshot(task) {
|
|
43
|
-
if (!tryTransitionTaskEvent(task, '
|
|
43
|
+
if (task.state === 'BLOCKED' && !tryTransitionTaskEvent(task, 'task.unblocked', { autonomousJob: false }).accepted)
|
|
44
44
|
throw lifecycleRejection(task, 'retryable after unblock');
|
|
45
|
-
|
|
45
|
+
if (!['QUEUED', 'DRAFT'].includes(task.state))
|
|
46
|
+
throw lifecycleRejection(task, 'retryable after unblock');
|
|
47
|
+
task.attempt = 0;
|
|
48
|
+
task.maxAttempts = Math.max(3, task.maxAttempts);
|
|
46
49
|
clearExecution(task);
|
|
47
50
|
delete task.blockedBy;
|
|
48
51
|
}
|
|
@@ -58,8 +61,12 @@ export async function applyTaskRetryAction(task, policy = {}) {
|
|
|
58
61
|
}
|
|
59
62
|
/** Fixed-job authoritative unblock continuation: legality is owned by the lifecycle graph. */
|
|
60
63
|
export async function applyTaskUnblockRetryAction(task) {
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
if (task.state === 'BLOCKED')
|
|
65
|
+
await transitionTaskAction(task, 'task.unblocked', 'retryable after unblock', { autonomousJob: false });
|
|
66
|
+
if (!['QUEUED', 'DRAFT'].includes(task.state))
|
|
67
|
+
throw lifecycleRejection(task, 'retryable after unblock');
|
|
68
|
+
task.attempt = 0;
|
|
69
|
+
task.maxAttempts = Math.max(3, task.maxAttempts);
|
|
63
70
|
clearExecution(task);
|
|
64
71
|
delete task.blockedBy;
|
|
65
72
|
return task;
|
package/dist/task-update.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function applyManagedTaskUpdate(store, jobId, taskId, proposed, exp
|
|
|
13
13
|
throw new Error('Task update identity mismatch');
|
|
14
14
|
if (current.updatedAt !== expectedUpdatedAt)
|
|
15
15
|
throw new Error('Task changed since update preflight; refresh and retry');
|
|
16
|
-
if (['RUNNING', '
|
|
16
|
+
if (['RUNNING', 'SUCCEEDED', 'CANCELLED'].includes(current.state) || current.executionId || current.leaseOwner)
|
|
17
17
|
throw new Error('Task is not mutable while active or terminal');
|
|
18
18
|
if (current.attempt > 0 && current.adapter !== proposed.adapter)
|
|
19
19
|
throw new Error('Cannot replace adapter after an execution attempt');
|
package/dist/troubleshooter.js
CHANGED
|
@@ -32,7 +32,7 @@ function belongsToCurrentRun(job, task) {
|
|
|
32
32
|
const slot = job.schedule?.currentRunSlot;
|
|
33
33
|
return !slot || task.createdAt >= slot;
|
|
34
34
|
}
|
|
35
|
-
const ACTIVE_RECOVERY_STATES = new Set(['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED'
|
|
35
|
+
const ACTIVE_RECOVERY_STATES = new Set(['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED']);
|
|
36
36
|
function recoveryTargetsTask(recovery, original) {
|
|
37
37
|
if (recovery.agentName !== 'troubleshooter')
|
|
38
38
|
return false;
|
package/dist/types.js
CHANGED
|
@@ -3,5 +3,16 @@ export const STORY_SIZE_TIMEOUT_MS = { S: 300000, M: 900000, L: 2700000 };
|
|
|
3
3
|
export function storySizeTimeoutMs(size) { return STORY_SIZE_TIMEOUT_MS[size]; }
|
|
4
4
|
export function taskOverallTimeoutMs(size, reasoning) { return storySizeTimeoutMs(size) * (reasoning?.toLowerCase() === 'high' ? 2 : 1); }
|
|
5
5
|
export function chatGptStartAckTimeoutMs(overallTimeoutMs) { return Math.min(600000, Math.max(120000, Math.floor(overallTimeoutMs / 3))); }
|
|
6
|
+
/** Normalize snapshots written before UNBLOCKED was removed from the lifecycle graph. */
|
|
7
|
+
export function normalizeLegacyTaskLifecycle(task, executionMode) {
|
|
8
|
+
const legacy = task;
|
|
9
|
+
if (legacy.state !== 'UNBLOCKED')
|
|
10
|
+
return task;
|
|
11
|
+
legacy.state = executionMode === 'autonomous' ? 'DRAFT' : 'QUEUED';
|
|
12
|
+
// Preserve the historical execution count when upgrading old snapshots.
|
|
13
|
+
task.maxAttempts = Math.max(3, task.maxAttempts ?? 0, task.attempt + 1);
|
|
14
|
+
task.unblock = task.unblock ? { ...task.unblock, event: 'task.unblocked' } : undefined;
|
|
15
|
+
return task;
|
|
16
|
+
}
|
|
6
17
|
export function assertJobLifecycleTransition(from, to) { assertV2StateTransition(jobLifecycleV2.graph, from, to, 'job'); }
|
|
7
18
|
export function assertTaskTransition(from, to) { assertV2StateTransition(taskLifecycleV2.graph, from, to, 'task'); }
|
package/dist/unblock.js
CHANGED
|
@@ -26,8 +26,8 @@ function unblockFacts(job, tasks, currentHeadSha, reason) {
|
|
|
26
26
|
return [...new Set(facts)];
|
|
27
27
|
}
|
|
28
28
|
export function unconsumedUnblockTrigger(job) {
|
|
29
|
-
const consumed = new Set((job.plans ?? []).flatMap(plan => plan.trigger.source === 'unblock' ? plan.trigger.taskIds ?? [] : []));
|
|
30
|
-
const tasks = job.tasks.filter(task => task.
|
|
29
|
+
const consumed = new Set((job.plans ?? []).filter(plan => plan.status === 'SUCCEEDED').flatMap(plan => plan.trigger.source === 'unblock' ? plan.trigger.taskIds ?? [] : []));
|
|
30
|
+
const tasks = job.tasks.filter(task => task.unblock && !consumed.has(task.id) && ['DRAFT', 'QUEUED', 'UNBLOCKED'].includes(task.state));
|
|
31
31
|
if (tasks.length === 0) {
|
|
32
32
|
const legacy = job.changeCycle;
|
|
33
33
|
if (!legacy || legacy.consumedByPlannerTaskId)
|
|
@@ -59,10 +59,10 @@ export async function unblockAutonomousTasks(options) {
|
|
|
59
59
|
if (!task || !isEffectivelyBlocked(task))
|
|
60
60
|
throw new Error(`Task ${requested.id} changed before authoritative unblock`);
|
|
61
61
|
const baseline = baselineFor(task);
|
|
62
|
-
if (!tryTransitionTaskEvent(task, 'task.unblocked').accepted)
|
|
62
|
+
if (!tryTransitionTaskEvent(task, 'task.unblocked', { autonomousJob: true }).accepted)
|
|
63
63
|
throw new Error(`Task ${task.id} cannot be authoritatively unblocked`);
|
|
64
64
|
task.blockedBaseline = baseline;
|
|
65
|
-
task.unblock = { at: now, source: trigger.source, reason: trigger.reason, baseline, currentHeadSha: currentHeadSha ?? undefined, facts, blockerJobId: trigger.blockerJobId, resolution: trigger.resolution };
|
|
65
|
+
task.unblock = { event: 'task.unblocked', at: now, source: trigger.source, reason: trigger.reason, baseline, currentHeadSha: currentHeadSha ?? undefined, facts, blockerJobId: trigger.blockerJobId, resolution: trigger.resolution };
|
|
66
66
|
task.updatedAt = now;
|
|
67
67
|
delete task.blockedBy;
|
|
68
68
|
}
|
|
@@ -98,16 +98,16 @@ export async function unblockTask(options) {
|
|
|
98
98
|
const baseline = baselineFor(options.task);
|
|
99
99
|
options.task.blockedBaseline = baseline;
|
|
100
100
|
const facts = unblockFacts(options.job, [options.task], currentHeadSha, options.trigger.reason);
|
|
101
|
-
options.task.unblock = { at: now, source: options.trigger.source, reason: options.trigger.reason, baseline, currentHeadSha: currentHeadSha ?? undefined, facts, blockerJobId: options.trigger.blockerJobId, resolution: options.trigger.resolution };
|
|
101
|
+
options.task.unblock = { event: 'task.unblocked', at: now, source: options.trigger.source, reason: options.trigger.reason, baseline, currentHeadSha: currentHeadSha ?? undefined, facts, blockerJobId: options.trigger.blockerJobId, resolution: options.trigger.resolution };
|
|
102
102
|
const apply = (current) => {
|
|
103
103
|
const task = current.tasks.find(item => item.id === options.task.id);
|
|
104
104
|
if (!task || !isEffectivelyBlocked(task))
|
|
105
105
|
throw new Error(`Task ${options.task.id} changed before authoritative unblock`);
|
|
106
|
-
if (!tryTransitionTaskEvent(task, 'task.unblocked').accepted)
|
|
106
|
+
if (!tryTransitionTaskEvent(task, 'task.unblocked', { autonomousJob: false }).accepted)
|
|
107
107
|
throw new Error(`Task ${task.id} cannot be authoritatively unblocked`);
|
|
108
108
|
const baseline = baselineFor(task);
|
|
109
109
|
task.blockedBaseline = baseline;
|
|
110
|
-
task.unblock = { at: now, source: options.trigger.source, reason: options.trigger.reason, baseline, currentHeadSha: currentHeadSha ?? undefined, facts, blockerJobId: options.trigger.blockerJobId, resolution: options.trigger.resolution };
|
|
110
|
+
task.unblock = { event: 'task.unblocked', at: now, source: options.trigger.source, reason: options.trigger.reason, baseline, currentHeadSha: currentHeadSha ?? undefined, facts, blockerJobId: options.trigger.blockerJobId, resolution: options.trigger.resolution };
|
|
111
111
|
applyTaskUnblockRetrySnapshot(task);
|
|
112
112
|
task.updatedAt = now;
|
|
113
113
|
delete task.blockedBy;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { GraphRuntime } from './graph-v2.js';
|
|
3
3
|
import { WORKFLOW_EFFECT_NAMES, WORKFLOW_EVENT_NAMES, WORKFLOW_MUTATION_NAMES, } from './contracts.js';
|
|
4
|
-
const TASK_STATES = ['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED', '
|
|
4
|
+
const TASK_STATES = ['DRAFT', 'QUEUED', 'RUNNING', 'BLOCKED', 'SUCCEEDED', 'FAILED', 'CANCELLED'];
|
|
5
5
|
const JOB_STATES = ['DRAFT', 'ACTIVE', 'COMPLETED', 'CANCELLED'];
|
|
6
6
|
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
7
7
|
function eventRegistry() {
|
|
@@ -27,6 +27,7 @@ export const taskLifecycleV2 = load('task-lifecycle', TASK_STATES, {
|
|
|
27
27
|
recoverableExecution: boolGuard('recoverableExecution'),
|
|
28
28
|
troubleshooterRecovery: boolGuard('troubleshooterRecovery'),
|
|
29
29
|
retryAvailable: boolGuard('retryAvailable'),
|
|
30
|
+
autonomousJob: boolGuard('autonomousJob'),
|
|
30
31
|
});
|
|
31
32
|
export const jobLifecycleV2 = load('job-lifecycle', JOB_STATES, {
|
|
32
33
|
notMerged: boolGuard('notMerged'),
|
|
@@ -52,9 +52,9 @@ export function plannerOrchestrationFacts(job, recoverablePendingPlan) {
|
|
|
52
52
|
hasSeedWork: !((job.plans?.length ?? 0) === 0 && job.tasks.filter(task => task.kind !== 'planner').length === 0),
|
|
53
53
|
noActivePlanner: !scopedPlans.some(plan => plan.status === 'PLANNING'),
|
|
54
54
|
noBlockedWork: Boolean(unconsumedUnblockTrigger(job)) || !scopedTasks.some(task => task.kind !== 'planner' && isEffectivelyBlocked(task)),
|
|
55
|
-
noActiveImplementation: !scopedTasks.some(task => task.kind !== 'planner' && ['
|
|
55
|
+
noActiveImplementation: !scopedTasks.some(task => task.kind !== 'planner' && (['QUEUED', 'RUNNING'].includes(task.state) || (task.state === 'DRAFT' && !task.unblock))),
|
|
56
56
|
resolutionOwnedByTroubleshooter: scopedTasks.some(task => task.kind !== 'planner' && task.resolution?.owner === 'troubleshooter' && ['active', 'waiting'].includes(task.resolution.state)),
|
|
57
|
-
continuationMaterialized: !(latestObjective === 'in_progress' && latestId && !job.tasks.some(task => task.kind !== 'planner' && task.planningDecisionId === latestId)),
|
|
57
|
+
continuationMaterialized: Boolean(unconsumedUnblockTrigger(job)) || !(latestObjective === 'in_progress' && latestId && !job.tasks.some(task => task.kind !== 'planner' && task.planningDecisionId === latestId)),
|
|
58
58
|
objectiveUnsatisfied: latestObjective !== 'satisfied',
|
|
59
59
|
};
|
|
60
60
|
}
|
|
@@ -33,6 +33,8 @@ export function reconcileTaskDependencies(job) {
|
|
|
33
33
|
for (const task of scopedTasks) {
|
|
34
34
|
if (task.kind === 'planner')
|
|
35
35
|
continue;
|
|
36
|
+
if (task.state === 'DRAFT' && task.unblock)
|
|
37
|
+
continue;
|
|
36
38
|
const ready = dependencyReady(task, tasks);
|
|
37
39
|
const facts = {
|
|
38
40
|
dependenciesSatisfied: task.dependencies.length > 0 && ready,
|
package/package.json
CHANGED
|
@@ -4,7 +4,6 @@ draft["state: DRAFT"]
|
|
|
4
4
|
queued["state: QUEUED"]
|
|
5
5
|
running["state: RUNNING"]
|
|
6
6
|
blocked["state: BLOCKED"]
|
|
7
|
-
unblocked["state: UNBLOCKED"]
|
|
8
7
|
succeeded["state: SUCCEEDED"]
|
|
9
8
|
failed["state: FAILED"]
|
|
10
9
|
cancelled["state: CANCELLED"]
|
|
@@ -76,6 +75,8 @@ completeClearError["action: mutation.clearError"]
|
|
|
76
75
|
completeClearReason["action: mutation.clearFailureReason"]
|
|
77
76
|
unblockRetryClearError["action: mutation.clearError"]
|
|
78
77
|
unblockRetryClearReason["action: mutation.clearFailureReason"]
|
|
78
|
+
unblockDraftClearError["action: mutation.clearError"]
|
|
79
|
+
unblockDraftClearReason["action: mutation.clearFailureReason"]
|
|
79
80
|
failedRetryClearError["action: mutation.clearError"]
|
|
80
81
|
failedRetryClearReason["action: mutation.clearFailureReason"]
|
|
81
82
|
recoverStartClearError["action: mutation.clearError"]
|
|
@@ -179,12 +180,15 @@ leaseFailRunningClearExecution --> failed
|
|
|
179
180
|
running -->|on: task.cancelled| cancelled
|
|
180
181
|
running -->|on: cancel| cancelled
|
|
181
182
|
|
|
182
|
-
|
|
183
|
-
blocked -->|on:
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
autonomousJob["guard: autonomousJob"]
|
|
184
|
+
blocked -->|on: task.unblocked| autonomousJob
|
|
185
|
+
autonomousJob -->|true| unblockDraftClearError
|
|
186
|
+
unblockDraftClearError --> unblockDraftClearReason
|
|
187
|
+
unblockDraftClearReason --> draft
|
|
188
|
+
autonomousJob -->|false| unblockRetryClearError
|
|
186
189
|
unblockRetryClearError --> unblockRetryClearReason
|
|
187
190
|
unblockRetryClearReason --> queued
|
|
191
|
+
blocked -->|on: cancel| cancelled
|
|
188
192
|
|
|
189
193
|
failed -->|on: queue| depSat
|
|
190
194
|
failed -->|on: retry| failedRetryClearError
|