agents-relay 2.0.156 → 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/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/blockers.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { applyTaskReleaseAction
|
|
2
|
-
import { isEffectivelyBlocked
|
|
1
|
+
import { applyTaskReleaseAction } from './task-actions.js';
|
|
2
|
+
import { isEffectivelyBlocked } from './unblock.js';
|
|
3
3
|
import { dispatchBlockerResolution } from './workflow/blocker-resolution-v2.js';
|
|
4
4
|
import { dispatchTaskResolutionSync, materializeResolution } from './workflow/task-resolution.js';
|
|
5
5
|
function dependentKey(value) {
|
|
@@ -99,47 +99,25 @@ export async function resolveBlockerDependencies(options) {
|
|
|
99
99
|
loadError = error instanceof Error ? error.message : String(error);
|
|
100
100
|
}
|
|
101
101
|
const targetMatches = !!task && ['DRAFT', 'FAILED', 'BLOCKED', 'QUEUED'].includes(task.state) && task.attempt === relation.blockedAttempt;
|
|
102
|
-
let note = 'dependent task
|
|
102
|
+
let note = 'dependent task released from dependency wait';
|
|
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
108
|
alreadyUnblocked: Boolean(task?.unblock && ['DRAFT', 'QUEUED'].includes(task.state)),
|
|
109
|
-
isDraft: task?.state === 'DRAFT', isBlocked: task
|
|
109
|
+
isDraft: task?.state === 'DRAFT', isBlocked: Boolean(task && isEffectivelyBlocked(task)),
|
|
110
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');
|
|
114
|
-
if (actionId === 'blocker.
|
|
115
|
-
await unblockAutonomousTasks({ store, job: targetJob, tasks: [task], trigger: { source: 'blocker_job', reason: 'Blocker Job ' + blocker.id + ' resolved (' + options.resolution + ')', blockerJobId: blocker.id, resolution: options.resolution }, now });
|
|
116
|
-
unblocked.push({ repository: relation.repository, prNumber: relation.prNumber, jobId: relation.jobId, taskId: relation.taskId });
|
|
117
|
-
note = 'dependent task unblocked for replanning';
|
|
118
|
-
}
|
|
119
|
-
else if (actionId === 'blocker.acceptExistingUnblock')
|
|
120
|
-
note = 'dependent Job already unblocked for replanning';
|
|
121
|
-
else if (actionId === 'blocker.releaseDraft') {
|
|
114
|
+
if (actionId === 'blocker.releaseDraft') {
|
|
122
115
|
await applyTaskReleaseAction(task);
|
|
123
116
|
task.error = null;
|
|
124
117
|
task.updatedAt = now;
|
|
125
118
|
delete task.blockedBy;
|
|
126
119
|
await store.saveTask(task);
|
|
127
120
|
}
|
|
128
|
-
else if (actionId === 'blocker.unblockTask') {
|
|
129
|
-
await unblockTask({ store, job: targetJob, task, trigger: { source: 'blocker_job', reason: 'Blocker Job ' + blocker.id + ' resolved (' + options.resolution + ')', blockerJobId: blocker.id, resolution: options.resolution }, now });
|
|
130
|
-
unblocked.push({ repository: relation.repository, prNumber: relation.prNumber, jobId: relation.jobId, taskId: relation.taskId });
|
|
131
|
-
}
|
|
132
|
-
else if (actionId === 'blocker.retryFailed') {
|
|
133
|
-
const result = await dispatchTaskAction(store, { id: 'task.retry', jobId: targetJob.id, taskId: task.id });
|
|
134
|
-
delete result.task.blockedBy;
|
|
135
|
-
result.task.updatedAt = now;
|
|
136
|
-
await store.saveTask(result.task);
|
|
137
|
-
}
|
|
138
|
-
else if (actionId === 'blocker.retryUnblocked') {
|
|
139
|
-
await applyTaskUnblockRetryAction(task);
|
|
140
|
-
task.updatedAt = now;
|
|
141
|
-
await store.saveTask(task);
|
|
142
|
-
}
|
|
143
121
|
else
|
|
144
122
|
throw new Error('Unsupported blocker graph action ' + actionId);
|
|
145
123
|
return {};
|
|
@@ -160,6 +138,7 @@ export async function resolveBlockerDependencies(options) {
|
|
|
160
138
|
relation.resolvedAt = now;
|
|
161
139
|
relation.resolution = options.resolution;
|
|
162
140
|
relation.note = note;
|
|
141
|
+
unblocked.push({ repository: relation.repository, prNumber: relation.prNumber, jobId: relation.jobId, taskId: relation.taskId });
|
|
163
142
|
resumed += 1;
|
|
164
143
|
}
|
|
165
144
|
const remaining = relations.filter(relation => relation.state === 'waiting');
|
|
@@ -32,7 +32,7 @@ export async function quarantineUnavailableCodexFallback(transitions, emit, jobI
|
|
|
32
32
|
mutate: (_job, task) => { task.error = `Codex fallback route became unavailable: ${error instanceof Error ? error.message : String(error)}`; },
|
|
33
33
|
});
|
|
34
34
|
if (transition?.decision.accepted)
|
|
35
|
-
await emit(eventFor(jobId, taskId, transition.task.parentTaskId, 'task.
|
|
35
|
+
await emit(eventFor(jobId, taskId, transition.task.parentTaskId, 'task.failed', 'failed', transition.task.error ?? 'Codex fallback route unavailable', 'user', { reason: transition.task.failureReason, phase: 'fallback-routing' }));
|
|
36
36
|
}
|
|
37
37
|
export function persistChatGptPlatformFailure(task, original, executionId, classification, evidence, safeToReroute) {
|
|
38
38
|
const route = original?.routing;
|
package/dist/cli-help.js
CHANGED
|
@@ -93,7 +93,7 @@ Example:
|
|
|
93
93
|
'job merge': `Request an idempotent external pull-request merge; return QUEUED until confirmed, when the Graph Engine completes the Job.
|
|
94
94
|
|
|
95
95
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. The current PR head SHA is pinned into the merge request.`,
|
|
96
|
-
'job unblock': `Authoritatively unblock
|
|
96
|
+
'job unblock': `Authoritatively unblock BLOCKED Troubleshooter Tasks in one Job.\n\nRequired: --repo OWNER/REPO --pr NUMBER --id JOB_ID --reason TEXT. Each unblocked Troubleshooter is queued as the same Task and must rerun, verify the condition, and issue a fresh resolution before retrying an original failed Task.`,
|
|
97
97
|
'job cancel': `Cancel the durable Job and close its backing pull request.
|
|
98
98
|
|
|
99
99
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. Nonterminal Tasks are cancelled before the PR is closed.`,
|
package/dist/cli-unblock.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { arg, publishTaskUnblocked, publishWake, saveLocal, withEventBus } from './cli-support.js';
|
|
2
|
-
import { isEffectivelyBlocked,
|
|
2
|
+
import { isEffectivelyBlocked, unblockTask } from './unblock.js';
|
|
3
3
|
import { resumeTroubleshooter } from './task-recovery.js';
|
|
4
4
|
export async function runTaskUnblockCommand(args, store, job, task, localFile) {
|
|
5
5
|
const reason = arg(args, '--reason');
|
|
@@ -29,24 +29,13 @@ export async function runJobUnblockCommand(args, store, job) {
|
|
|
29
29
|
const blocked = job.tasks.filter(isEffectivelyBlocked);
|
|
30
30
|
if (blocked.length === 0)
|
|
31
31
|
throw new Error(`Job ${job.id} has no effective BLOCKED work task`);
|
|
32
|
-
if (job.executionMode === 'autonomous') {
|
|
33
|
-
const planTrigger = await unblockAutonomousTasks({ store, job, tasks: blocked, trigger: { source: 'cli', reason } });
|
|
34
|
-
const refreshed = await store.load(job.id);
|
|
35
|
-
await withEventBus(args, async (bus) => {
|
|
36
|
-
for (const task of blocked)
|
|
37
|
-
await publishTaskUnblocked(bus, refreshed, task.id, reason);
|
|
38
|
-
await publishWake(bus, refreshed, 'job', `Job ${job.id} unblocked for replanning`);
|
|
39
|
-
});
|
|
40
|
-
console.log(JSON.stringify({ jobId: job.id, replanning: true, planTrigger }, null, 2));
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
32
|
for (const task of blocked)
|
|
44
33
|
await unblockTask({ store, job, task, trigger: { source: 'cli', reason } });
|
|
45
34
|
const refreshed = await store.load(job.id);
|
|
46
35
|
await withEventBus(args, async (bus) => {
|
|
47
36
|
for (const task of blocked)
|
|
48
37
|
await publishTaskUnblocked(bus, refreshed, task.id, reason);
|
|
49
|
-
await publishWake(bus, refreshed, 'job', `Job ${job.id}
|
|
38
|
+
await publishWake(bus, refreshed, 'job', `Troubleshooter work in Job ${job.id} resumed`);
|
|
50
39
|
});
|
|
51
|
-
console.log(JSON.stringify({ jobId: job.id,
|
|
40
|
+
console.log(JSON.stringify({ jobId: job.id, resumedTasks: blocked.map(task => task.id) }, null, 2));
|
|
52
41
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { applyJobGraphTarget
|
|
2
|
-
import { jobLifecycleV2
|
|
1
|
+
import { applyJobGraphTarget } from './workflow/runtime.js';
|
|
2
|
+
import { jobLifecycleV2 } from './workflow/definitions-v2.js';
|
|
3
3
|
import { dispatchWorkflowTargetOverride } from './workflow/dispatcher.js';
|
|
4
4
|
import { dispatchTaskAction } from './task-actions.js';
|
|
5
5
|
import { applyJobCancelAction } from './job-actions.js';
|
|
@@ -45,22 +45,12 @@ export function dashboardTaskSetState(store) {
|
|
|
45
45
|
throw new Error(`Task ${taskId} not found`);
|
|
46
46
|
if (task.state === state)
|
|
47
47
|
return job;
|
|
48
|
-
if (
|
|
49
|
-
throw new Error(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
});
|
|
56
|
-
task.manualStateOverrides = [
|
|
57
|
-
...(task.manualStateOverrides ?? []),
|
|
58
|
-
{ from, to: state, at: now, actor: 'dashboard-user' },
|
|
59
|
-
].slice(-20);
|
|
60
|
-
task.leaseOwner = null;
|
|
61
|
-
task.leaseExpiresAt = null;
|
|
62
|
-
task.executionId = null;
|
|
63
|
-
await store.saveTask(task);
|
|
64
|
-
return await store.load(job.id);
|
|
48
|
+
if (state !== 'CANCELLED')
|
|
49
|
+
throw new Error('Task lifecycle state is managed by Relay; use retry, cancel, or the Troubleshooter response flow');
|
|
50
|
+
if (task.state !== 'CANCELLED') {
|
|
51
|
+
await dispatchTaskAction(store, { id: 'task.cancel', jobId, taskId });
|
|
52
|
+
return await store.load(job.id);
|
|
53
|
+
}
|
|
54
|
+
return job;
|
|
65
55
|
};
|
|
66
56
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { eventsBusHistory } from './events-bus-runtime.js';
|
|
5
|
+
const repositoryPattern = /^([A-Za-z0-9](?:[A-Za-z0-9-]{0,36}[A-Za-z0-9])?)\/([A-Za-z0-9._-]{1,100})$/;
|
|
6
|
+
const workflowDirectory = fileURLToPath(new URL('../workflow/', import.meta.url));
|
|
7
|
+
export async function dashboardWorkflowGraphs() {
|
|
8
|
+
const names = (await readdir(workflowDirectory)).filter(name => name.endsWith('.mmd')).sort();
|
|
9
|
+
return Promise.all(names.map(async (name) => {
|
|
10
|
+
const source = await readFile(join(workflowDirectory, name), 'utf8');
|
|
11
|
+
const metadata = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(source)?.[1] ?? '';
|
|
12
|
+
const field = (key) => metadata.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))?.[1]?.trim() ?? '';
|
|
13
|
+
return {
|
|
14
|
+
id: name.replace(/\.mmd$/, ''),
|
|
15
|
+
name: field('title') || (name === 'main.mmd' ? 'Main / Happy Path' : name.replace(/\.mmd$/, '').split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(' ')),
|
|
16
|
+
description: field('description'), path: `workflow/${name}`, source,
|
|
17
|
+
};
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
export function githubPrUrl(repository, prNumber) {
|
|
21
|
+
if (typeof repository !== 'string' || typeof prNumber !== 'number' || !Number.isInteger(prNumber) || prNumber <= 0)
|
|
22
|
+
return null;
|
|
23
|
+
const match = repositoryPattern.exec(repository);
|
|
24
|
+
if (!match)
|
|
25
|
+
return null;
|
|
26
|
+
const [, owner, repo] = match;
|
|
27
|
+
if ([owner, repo].some(segment => segment === '.' || segment === '..'))
|
|
28
|
+
return null;
|
|
29
|
+
return `https://github.com/${owner}/${repo}/pull/${prNumber}`;
|
|
30
|
+
}
|
|
31
|
+
export function effectiveGitHubUsage(snapshot, runtime) {
|
|
32
|
+
const candidates = [snapshot?.resources.core, snapshot?.resources.graphql, snapshot?.resources.search, runtime?.resources.core, runtime?.resources.graphql, runtime?.resources.search]
|
|
33
|
+
.filter((candidate) => candidate !== undefined && candidate !== null)
|
|
34
|
+
.filter(candidate => Number.isFinite(candidate.limit) && Number(candidate.limit) > 0 && Number.isFinite(candidate.remaining) && Number.isFinite(candidate.reset));
|
|
35
|
+
const lowest = candidates.reduce((current, candidate) => !current || Number(candidate.remaining) / Number(candidate.limit) < Number(current.remaining) / Number(current.limit) ? candidate : current, null);
|
|
36
|
+
return lowest ? { limit: Number(lowest.limit), remaining: Number(lowest.remaining), reset: Number(lowest.reset) } : null;
|
|
37
|
+
}
|
|
38
|
+
export async function eventHistory(jobId, taskId, limit) {
|
|
39
|
+
try {
|
|
40
|
+
return await eventsBusHistory(jobId, taskId, limit);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/dashboard.js
CHANGED
|
@@ -2,67 +2,17 @@ import { inspectWorkflowSymbol } from './dashboard-workflow-inspection.js';
|
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { subscribeLocalCommits } from './local-commit-events.js';
|
|
4
4
|
import { handleTroubleshooterQa } from './troubleshooter-qa-api.js';
|
|
5
|
-
import { readFile, readdir } from 'node:fs/promises';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
5
|
import { readUsageSnapshot } from './usage.js';
|
|
9
6
|
import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
10
7
|
import { DeliveryDeduper, parseGitHubWebhook, verifyGitHubWebhook } from './github-webhook.js';
|
|
11
8
|
import { loadExecutionRouterRules, saveExecutionRouterRules } from './routing-policy.js';
|
|
12
9
|
import { summarizeRuntime } from './runtime-summary.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
10
|
+
import { dashboardWorkflowGraphs, effectiveGitHubUsage, eventHistory } from './dashboard-data.js';
|
|
11
|
+
export { dashboardWorkflowGraphs, githubPrUrl } from './dashboard-data.js';
|
|
15
12
|
const SSE_HEARTBEAT_MS = 15000;
|
|
16
|
-
const WORKFLOW_DIR = fileURLToPath(new URL('../workflow/', import.meta.url));
|
|
17
|
-
export async function dashboardWorkflowGraphs() {
|
|
18
|
-
const names = (await readdir(WORKFLOW_DIR)).filter(name => name.endsWith('.mmd')).sort();
|
|
19
|
-
return Promise.all(names.map(async (name) => {
|
|
20
|
-
const source = await readFile(join(WORKFLOW_DIR, name), 'utf8');
|
|
21
|
-
const metadata = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(source)?.[1] ?? '';
|
|
22
|
-
const field = (key) => metadata.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))?.[1]?.trim() ?? '';
|
|
23
|
-
return {
|
|
24
|
-
id: name.replace(/\.mmd$/, ''),
|
|
25
|
-
name: field('title') || (name === 'main.mmd' ? 'Main / Happy Path' : name.replace(/\.mmd$/, '').split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(' ')),
|
|
26
|
-
description: field('description'),
|
|
27
|
-
path: `workflow/${name}`, source,
|
|
28
|
-
};
|
|
29
|
-
}));
|
|
30
|
-
}
|
|
31
|
-
function effectiveGitHubUsage(snapshot, runtime) {
|
|
32
|
-
const candidates = [snapshot?.resources.core, snapshot?.resources.graphql, snapshot?.resources.search, runtime?.resources.core, runtime?.resources.graphql, runtime?.resources.search]
|
|
33
|
-
.filter((candidate) => candidate !== undefined && candidate !== null)
|
|
34
|
-
.filter(candidate => Number.isFinite(candidate.limit) && Number(candidate.limit) > 0 && Number.isFinite(candidate.remaining) && Number.isFinite(candidate.reset));
|
|
35
|
-
const lowest = candidates.reduce((current, candidate) => !current || Number(candidate.remaining) / Number(candidate.limit) < Number(current.remaining) / Number(current.limit) ? candidate : current, null);
|
|
36
|
-
return lowest ? { limit: Number(lowest.limit), remaining: Number(lowest.remaining), reset: Number(lowest.reset) } : null;
|
|
37
|
-
}
|
|
38
|
-
function isPositiveInteger(candidate) {
|
|
39
|
-
return typeof candidate === 'number' && Number.isInteger(candidate) && candidate > 0;
|
|
40
|
-
}
|
|
41
13
|
function isTaskId(candidate) {
|
|
42
14
|
return candidate.length > 0;
|
|
43
15
|
}
|
|
44
|
-
function isSafeRepositorySegment(segment) {
|
|
45
|
-
return segment !== '.' && segment !== '..';
|
|
46
|
-
}
|
|
47
|
-
export function githubPrUrl(repository, prNumber) {
|
|
48
|
-
if (typeof repository !== 'string' || !isPositiveInteger(prNumber))
|
|
49
|
-
return null;
|
|
50
|
-
const match = GITHUB_REPOSITORY_PATTERN.exec(repository);
|
|
51
|
-
if (!match)
|
|
52
|
-
return null;
|
|
53
|
-
const [, owner, repo] = match;
|
|
54
|
-
if (!isSafeRepositorySegment(owner) || !isSafeRepositorySegment(repo))
|
|
55
|
-
return null;
|
|
56
|
-
return `https://github.com/${owner}/${repo}/pull/${prNumber}`;
|
|
57
|
-
}
|
|
58
|
-
async function eventHistory(jobId, taskId, limit) {
|
|
59
|
-
try {
|
|
60
|
-
return await eventsBusHistory(jobId, taskId, limit);
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
16
|
export { dashboardHtml, dashboardVersion, dashboardSourceRevision } from './dashboard-html.js';
|
|
67
17
|
import { dashboardHtml, dashboardVersion } from './dashboard-html.js';
|
|
68
18
|
export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobId, usageRegistry, overviewProvider, webhook, statusProvider, githubUsageProvider, exactJobProvider, actions, routerTestProvider, decisionAuditProvider) {
|
|
@@ -51,8 +51,8 @@ export function recordHostInvocationDecision(db, delivery, fingerprint, writeSna
|
|
|
51
51
|
const job = snapshot?.json ? JSON.parse(snapshot.json) : null;
|
|
52
52
|
const task = job?.tasks.find(item => item.id === delivery.taskId);
|
|
53
53
|
const invocation = task?.interactiveInvocation;
|
|
54
|
-
if (!job || !task || !invocation || !(['RUNNING', '
|
|
55
|
-
(task.executionId !== delivery.executionId && task.state !== '
|
|
54
|
+
if (!job || !task || !invocation || !(['RUNNING', 'DRAFT'].includes(task.state)) || invocation.executionId !== delivery.executionId ||
|
|
55
|
+
(task.executionId !== delivery.executionId && task.state !== 'DRAFT') ||
|
|
56
56
|
invocation.status !== 'awaiting_decision' || invocation.invocationId !== delivery.invocationId || invocation.revision !== delivery.revision) {
|
|
57
57
|
recordHostInvocationEvent(db, delivery, 'stale', fingerprint);
|
|
58
58
|
db.exec('COMMIT');
|
|
@@ -167,7 +167,7 @@ export class HostInteractiveInvocationCoordinator {
|
|
|
167
167
|
const job = await this.options.store.loadJob(delivery.jobId);
|
|
168
168
|
const task = job?.tasks.find((item) => item.id === delivery.taskId);
|
|
169
169
|
if (!job || !task || task.interactiveInvocation?.executionId !== delivery.executionId ||
|
|
170
|
-
(task.executionId !== delivery.executionId && task.state !== '
|
|
170
|
+
(task.executionId !== delivery.executionId && task.state !== 'DRAFT')) {
|
|
171
171
|
await this.options.store.recordHostInvocationEvent?.(delivery, 'stale', interactiveDecisionFingerprint(delivery));
|
|
172
172
|
return { outcome: 'stale', reason: 'execution_not_bound' };
|
|
173
173
|
}
|
|
@@ -8,13 +8,13 @@ export async function resumeInteractiveTask(store, jobId, taskId, invocationId,
|
|
|
8
8
|
await store.mutateJob(jobId, job => {
|
|
9
9
|
const task = job.tasks.find(item => item.id === taskId);
|
|
10
10
|
const invocation = task?.interactiveInvocation;
|
|
11
|
-
if (!task || task.state !== '
|
|
11
|
+
if (!task || task.state !== 'DRAFT' || !invocation ||
|
|
12
12
|
invocation.invocationId !== invocationId || invocation.revision !== revision ||
|
|
13
13
|
invocation.status !== decision || !invocation.decidedAt)
|
|
14
14
|
return job;
|
|
15
|
-
//
|
|
16
|
-
if (!tryTransitionTaskEvent(task, '
|
|
17
|
-
throw new Error('Interactive Task
|
|
15
|
+
// Accepted interactive decisions release the same waiting Task from DRAFT.
|
|
16
|
+
if (!tryTransitionTaskEvent(task, 'release').accepted) {
|
|
17
|
+
throw new Error('Interactive Task release rejected by canonical lifecycle Graph');
|
|
18
18
|
}
|
|
19
19
|
applyTaskUnblockRetrySnapshot(task);
|
|
20
20
|
task.error = null;
|
|
@@ -30,7 +30,7 @@ export async function recoverInteractiveDecisions(store, jobId) {
|
|
|
30
30
|
const job = await store.load(jobId);
|
|
31
31
|
let recovered = false;
|
|
32
32
|
for (const task of job.tasks) {
|
|
33
|
-
if (task.state !== '
|
|
33
|
+
if (task.state !== 'DRAFT' || !task.interactiveInvocation?.decidedAt)
|
|
34
34
|
continue;
|
|
35
35
|
const invocation = task.interactiveInvocation;
|
|
36
36
|
if (invocation.status === 'awaiting_decision' || !await shouldRecoverInteractiveDecision(invocation.status))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { normalizeLegacyTaskLifecycle } from './types.js';
|
|
2
|
+
/** Move pre-contract ordinary BLOCKED snapshots to FAILED without touching the
|
|
3
|
+
* independent append-only event history. Unknown execution evidence is kept. */
|
|
4
|
+
export function migrateLegacyBlockedTasks(db) {
|
|
5
|
+
const rows = db.prepare('SELECT job_id,json FROM job_snapshot').all();
|
|
6
|
+
const update = db.prepare('UPDATE job_snapshot SET json=? WHERE job_id=?');
|
|
7
|
+
for (const row of rows) {
|
|
8
|
+
const job = JSON.parse(row.json);
|
|
9
|
+
const before = JSON.stringify(job.tasks);
|
|
10
|
+
job.tasks = job.tasks.map(task => normalizeLegacyTaskLifecycle(task, job.executionMode));
|
|
11
|
+
if (JSON.stringify(job.tasks) !== before)
|
|
12
|
+
update.run(JSON.stringify(job), row.job_id);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -6,7 +6,7 @@ export async function repairLegacyOrchestrators(jobId, now, ports) {
|
|
|
6
6
|
const { store } = ports;
|
|
7
7
|
const repairOne = async (taskId) => {
|
|
8
8
|
if (store.mutateJob) {
|
|
9
|
-
let outcome = { repaired: false, externallyCompleted: false,
|
|
9
|
+
let outcome = { repaired: false, externallyCompleted: false, failed: false, ageMs: 0 };
|
|
10
10
|
return await store.mutateJob(jobId, current => {
|
|
11
11
|
const task = current.tasks.find(item => item.id === taskId);
|
|
12
12
|
if (!task)
|
|
@@ -40,12 +40,12 @@ export async function repairLegacyOrchestrators(jobId, now, ports) {
|
|
|
40
40
|
const job = await store.load(jobId);
|
|
41
41
|
const task = job.tasks.find(item => item.id === stale.id);
|
|
42
42
|
const type = repair.externallyCompleted ? 'task.external_completion_applied'
|
|
43
|
-
: repair.
|
|
44
|
-
const status = repair.externallyCompleted ? 'succeeded' : repair.
|
|
43
|
+
: repair.failed ? 'task.orchestrator_quarantined' : 'task.orchestrator_rescued';
|
|
44
|
+
const status = repair.externallyCompleted ? 'succeeded' : repair.failed ? 'failed' : 'queued';
|
|
45
45
|
await ports.emit(eventFor(job.id, task.id, task.parentTaskId, type, status, repair.externallyCompleted
|
|
46
46
|
? `Applied durable external completion for legacy orchestrator task ${task.id}`
|
|
47
|
-
: repair.
|
|
48
|
-
? `
|
|
49
|
-
: `Recovered legacy orchestrator task ${task.id} through Execution Router`, 'orchestrator', { ageMs: repair.ageMs, adapter: repair.externallyCompleted || repair.
|
|
47
|
+
: repair.failed
|
|
48
|
+
? `Failed legacy RUNNING orchestrator task ${task.id}; worker outcome is unknown and Troubleshooter diagnosis is required`
|
|
49
|
+
: `Recovered legacy orchestrator task ${task.id} through Execution Router`, 'orchestrator', { ageMs: repair.ageMs, adapter: repair.externallyCompleted || repair.failed ? undefined : 'execution-router' }, { agent: 'relay', component: 'agents-relay' }));
|
|
50
50
|
}
|
|
51
51
|
}
|
package/dist/planner.js
CHANGED
|
@@ -23,7 +23,7 @@ export function semanticTaskTree(tasks) {
|
|
|
23
23
|
}
|
|
24
24
|
function plannerTrigger(job, manual = false) {
|
|
25
25
|
const consumed = new Set((job.plans ?? []).filter(plan => plan.status === 'SUCCEEDED').flatMap(plan => plan.trigger.source === 'unblock' ? plan.trigger.taskIds ?? [] : []));
|
|
26
|
-
const unblocked = job.tasks.filter(task => ['DRAFT', 'QUEUED'
|
|
26
|
+
const unblocked = job.tasks.filter(task => ['DRAFT', 'QUEUED'].includes(task.state) && task.unblock && !consumed.has(task.id));
|
|
27
27
|
const hasUnblock = unblocked.length > 0 || Boolean(job.changeCycle && !job.changeCycle.consumedByPlannerTaskId);
|
|
28
28
|
const decision = dispatchPlannerTrigger({
|
|
29
29
|
manualPlan: manual && !hasUnblock,
|
|
@@ -13,7 +13,7 @@ import { createWorkerStartAck } from './worker-start-ack.js';
|
|
|
13
13
|
import { monitorExecutionProcess } from './execution-process-tracker.js';
|
|
14
14
|
import { HostInteractiveInvocationCoordinator, hostInvocationPorts } from './interactive-invocation.js';
|
|
15
15
|
import { resumeInteractiveTask } from './interactive-resume.js';
|
|
16
|
-
import {
|
|
16
|
+
import { persistChatGptPlatformFailure } from './chatgpt-platform-fallback.js';
|
|
17
17
|
import { acceptTaskProgress, armInactivityWatchdog, inactivityDeadline, isCorrelatedWorkerEvent, isRecoverableWatchdogFailure } from './task-inactivity-watchdog.js';
|
|
18
18
|
const activeExecutionOwners = new Map();
|
|
19
19
|
import { withExecutionBoundary } from './execution-boundary.js';
|
|
@@ -257,7 +257,10 @@ export class ReconcilerRuntimeBase {
|
|
|
257
257
|
const message = failure instanceof Error ? failure.message : String(failure);
|
|
258
258
|
const currentJob = await this.store.load(jobId);
|
|
259
259
|
const currentTask = currentJob.tasks.find(task => task.id === taskId);
|
|
260
|
+
const activeExecution = this.live.get(taskId);
|
|
260
261
|
const platformFailure = currentTask?.adapter === 'chatgpt' ? classifyChatGptPlatformFailure(failure, phase) : undefined;
|
|
262
|
+
const unknownOutcome = phase === 'start-ack' || ['outcome_unknown', 'start_ack_timeout'].includes(platformFailure?.classification ?? '')
|
|
263
|
+
|| (phase === 'delivery' && activeExecution?.executionId === executionId && activeExecution.workerStarted);
|
|
261
264
|
const failureReason = phase === 'start-ack' ? 'worker_start_timeout'
|
|
262
265
|
: /WEBHELM_UNAVAILABLE|webhelm runtime dependency unavailable|webhelm session start returned no session_id|no close frame received or sent/i.test(message)
|
|
263
266
|
? 'browser_workspace_unavailable'
|
|
@@ -269,19 +272,14 @@ export class ReconcilerRuntimeBase {
|
|
|
269
272
|
const cleanup = setTimeout(() => this.launchFailureClaims.delete(claim), 60000);
|
|
270
273
|
cleanup.unref();
|
|
271
274
|
const workflowEvent = platformFailure ? 'worker.platform_failure' : phase === 'start-ack' ? 'worker.start_ack_failed' : 'worker.delivery_failed';
|
|
272
|
-
const usage = this.options.usageSnapshotProvider ? await this.options.usageSnapshotProvider() : undefined;
|
|
273
|
-
const platformWorkerCompatible = Boolean(platformFailure?.safeToReroute && currentTask && !(currentTask.agentName === 'reviewer' && this.options.reviewer?.threadId)
|
|
274
|
-
&& await codexFallbackCompatible(currentTask, this.options.adapters, await this.store.listAgents(), usage));
|
|
275
275
|
const transition = await this.taskTransitions.apply(jobId, taskId, platformFailure ? 'worker.platform_failure' : workflowEvent, {
|
|
276
276
|
eligible: (_job, task) => task.executionId === executionId,
|
|
277
|
-
facts: (_job, task) => ({ platformFallbackSafe: task.state === 'QUEUED' && Boolean(platformFailure?.safeToReroute && platformWorkerCompatible && task.attempt < task.maxAttempts) }),
|
|
278
277
|
mutate: (_job, task, decision) => {
|
|
279
278
|
if (platformFailure)
|
|
280
279
|
persistChatGptPlatformFailure(task, currentTask, executionId, platformFailure.classification, platformFailure.evidence, platformFailure.safeToReroute);
|
|
281
280
|
task.error = decision.target === 'QUEUED' ? null : `Worker ${phase} failed: ${message}`;
|
|
282
|
-
task.failureReason =
|
|
283
|
-
?
|
|
284
|
-
: failureReason;
|
|
281
|
+
task.failureReason = unknownOutcome
|
|
282
|
+
? 'worker_outcome_unknown' : platformFailure ? 'worker_platform_failure' : failureReason;
|
|
285
283
|
task.leaseOwner = null;
|
|
286
284
|
task.leaseExpiresAt = null;
|
|
287
285
|
},
|
|
@@ -300,14 +298,8 @@ export class ReconcilerRuntimeBase {
|
|
|
300
298
|
this.clearExecution(taskId, executionId);
|
|
301
299
|
}
|
|
302
300
|
}
|
|
303
|
-
const runtimeRetry = transition.decision.target === 'QUEUED' ? transition.task : null;
|
|
304
301
|
const event = eventFor(jobId, taskId, transition.task.parentTaskId, 'task.failed', 'failed', transition.task.error ?? `Worker ${phase} failed: ${message}`, 'user', { phase, reason: transition.task.failureReason ?? failureReason, workflowEvent, ...(transition.task.platformFailure ? { platformFailure: transition.task.platformFailure } : {}) }, { agent: 'relay', component: 'agents-relay', watchdog: true });
|
|
305
302
|
this.detach(this.emit(event), jobId, taskId, 'worker failure event');
|
|
306
|
-
if (runtimeRetry) {
|
|
307
|
-
this.detach(this.emit(eventFor(jobId, taskId, runtimeRetry.parentTaskId, 'task.rerouting', 'queued', 'ChatGPT platform rejection recorded; routing the same Task through Execution Router', 'orchestrator', { previousFailure: transition.task.platformFailure?.classification, executionId, attempt: runtimeRetry.attempt, maxAttempts: runtimeRetry.maxAttempts })), jobId, taskId, 'runtime retry event');
|
|
308
|
-
this.detach(this.eventCoordinator.accept({ jobId, reason: 'webhelm-runtime-retry' }), jobId, taskId, 'runtime retry reconcile wake');
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
303
|
this.detach(this.eventCoordinator.accept({ jobId, reason: `worker-${phase}-failure` }), jobId, taskId, 'worker failure reconcile wake');
|
|
312
304
|
}
|
|
313
305
|
async runtimeSettled(jobId, taskId, executionId, result, error) {
|
|
@@ -403,11 +395,15 @@ export class ReconcilerRuntimeBase {
|
|
|
403
395
|
async applyTerminalEvent(event, trustedInternal = false) {
|
|
404
396
|
const liveAtTerminal = this.live.get(event.task_id);
|
|
405
397
|
const terminalEventExecutionId = executionIdFromSource(eventSourceId(event), event.job_id, event.task_id);
|
|
406
|
-
const
|
|
398
|
+
const lifecycleEvent = event.type === 'task.blocked'
|
|
399
|
+
? (await this.store.load(event.job_id)).tasks.find(task => task.id === event.task_id)?.interactiveInvocation?.status === 'awaiting_decision'
|
|
400
|
+
? 'interactive.waiting' : event.type
|
|
401
|
+
: event.type;
|
|
402
|
+
const transition = await withExecutionBoundary(event.job_id, event.task_id, () => this.taskTransitions.apply(event.job_id, event.task_id, lifecycleEvent, {
|
|
407
403
|
eligible: (_job, task) => {
|
|
408
404
|
if (!['codex', 'chatgpt', 'orchestrator'].includes(task.adapter))
|
|
409
405
|
return false;
|
|
410
|
-
if (event.type === 'task.blocked' && (!trustedInternal || task.agentName !== 'troubleshooter')
|
|
406
|
+
if (event.type === 'task.blocked' && lifecycleEvent !== 'interactive.waiting' && (!trustedInternal || task.agentName !== 'troubleshooter'))
|
|
411
407
|
return false;
|
|
412
408
|
if (task.agentName === 'reviewer' && event.type === 'task.completed')
|
|
413
409
|
return false;
|
|
@@ -426,7 +422,7 @@ export class ReconcilerRuntimeBase {
|
|
|
426
422
|
facts: (_job, task) => {
|
|
427
423
|
const processExitedAfterPublishedTerminal = task.failureReason === 'worker_exit_without_terminal_event';
|
|
428
424
|
const freshEnough = processExitedAfterPublishedTerminal || isCorrelatedWorkerEvent(task, event);
|
|
429
|
-
return { recoverableExecution: isRecoverableWatchdogFailure(task) && freshEnough };
|
|
425
|
+
return { recoverableExecution: isRecoverableWatchdogFailure(task) && freshEnough, awaitingInteractiveDecision: lifecycleEvent === 'interactive.waiting' && task.interactiveInvocation?.status === 'awaiting_decision' };
|
|
430
426
|
},
|
|
431
427
|
mutate: (job, task) => {
|
|
432
428
|
const modelBacked = task.adapter === 'codex' || task.adapter === 'chatgpt';
|
|
@@ -457,7 +453,7 @@ export class ReconcilerRuntimeBase {
|
|
|
457
453
|
task.error = event.message || `Task ${task.id} failed`;
|
|
458
454
|
task.failureReason = typeof event.data?.reason === 'string' ? event.data.reason : undefined;
|
|
459
455
|
}
|
|
460
|
-
else if (event.type === 'task.blocked') {
|
|
456
|
+
else if (event.type === 'task.blocked' && lifecycleEvent !== 'interactive.waiting') {
|
|
461
457
|
task.error = event.message || `Task ${task.id} blocked`;
|
|
462
458
|
const data = event.data ?? {};
|
|
463
459
|
const head = ['candidate_head', 'candidate_head_sha', 'pr_head', 'head_sha']
|
|
@@ -484,7 +480,7 @@ export class ReconcilerRuntimeBase {
|
|
|
484
480
|
if (event.type === 'task.completed')
|
|
485
481
|
await this.deliverContinuation(transition.job, transition.task);
|
|
486
482
|
const reviewWorker = transition.task.agentName === 'reviewer';
|
|
487
|
-
return { applied: true, reconcile: !reviewWorker || event.type === 'task.failed', reason: reviewWorker ? `${event.type}.review-worker` : event.type, reconcileMode: reviewWorker ? undefined : 'detached' };
|
|
483
|
+
return { applied: true, reconcile: lifecycleEvent !== 'interactive.waiting' && (!reviewWorker || event.type === 'task.failed'), reason: reviewWorker ? `${event.type}.review-worker` : event.type, reconcileMode: reviewWorker || lifecycleEvent === 'interactive.waiting' ? undefined : 'detached' };
|
|
488
484
|
}
|
|
489
485
|
terminalWait(taskId, executionId, launchBarrier) {
|
|
490
486
|
let resolve;
|
package/dist/relay-store.js
CHANGED
|
@@ -2,6 +2,7 @@ import { loadRelayJob, listRelayJobs } from './relay-job-snapshot-reads.js';
|
|
|
2
2
|
import { SQLiteWorkflowOperations } from './relay-workflow-operations.js';
|
|
3
3
|
import { preserveTaskComments } from './task-comments.js';
|
|
4
4
|
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
import { migrateLegacyBlockedTasks } from './legacy-blocked-task-migration.js';
|
|
5
6
|
import { dirname } from 'node:path';
|
|
6
7
|
import { mkdirSync } from 'node:fs';
|
|
7
8
|
import { recordPublicationInDatabase, loadPublicationFromDatabase, listPublicationEventsFromDatabase } from './publication-store.js';
|
|
@@ -70,6 +71,7 @@ export class SQLiteRelayStore {
|
|
|
70
71
|
}
|
|
71
72
|
else
|
|
72
73
|
this.migrate();
|
|
74
|
+
migrateLegacyBlockedTasks(this.db);
|
|
73
75
|
this.outbox = new ProjectionOutbox(this.db, scopeJobId);
|
|
74
76
|
this.outbox.resetProcessing(new Date().toISOString());
|
|
75
77
|
}
|
package/dist/relayd-control.js
CHANGED
|
@@ -124,11 +124,8 @@ export function createWorkspaceControl(deps) {
|
|
|
124
124
|
const blocked = job.tasks.filter(isEffectivelyBlocked);
|
|
125
125
|
if (!blocked.length)
|
|
126
126
|
throw new Error(`Job ${job.id} has no effective BLOCKED work task`);
|
|
127
|
-
|
|
128
|
-
await
|
|
129
|
-
else
|
|
130
|
-
for (const task of blocked)
|
|
131
|
-
await unblockTask({ store, job, task, trigger: { source: 'cli', reason } });
|
|
127
|
+
for (const task of blocked)
|
|
128
|
+
await unblockTask({ store, job, task, trigger: { source: 'cli', reason } });
|
|
132
129
|
const refreshed = await store.load(jobId);
|
|
133
130
|
for (const task of blocked)
|
|
134
131
|
await publishTaskUnblocked(bus, refreshed, task.id, reason);
|
package/dist/task-actions.js
CHANGED
|
@@ -186,8 +186,8 @@ export async function dispatchTaskAction(store, request) {
|
|
|
186
186
|
const task = current.tasks.find(item => item.id === request.taskId);
|
|
187
187
|
if (!task)
|
|
188
188
|
throw new Error(`Task ${request.taskId} not found`);
|
|
189
|
+
const recovery = request.id === TASK_RECOVER_ACTION && request.recoveryTaskId ? current.tasks.find(item => item.id === request.recoveryTaskId) : undefined;
|
|
189
190
|
if (request.id === TASK_RECOVER_ACTION) {
|
|
190
|
-
const recovery = request.recoveryTaskId ? current.tasks.find(item => item.id === request.recoveryTaskId) : undefined;
|
|
191
191
|
if (!recovery || recovery.agentName !== 'troubleshooter' || recovery.parentTaskId !== task.id || !['QUEUED', 'RUNNING'].includes(recovery.state))
|
|
192
192
|
throw new Error(`task recover requires an active Troubleshooter recovery for ${task.id}`);
|
|
193
193
|
}
|
|
@@ -198,7 +198,11 @@ export async function dispatchTaskAction(store, request) {
|
|
|
198
198
|
task.workerSelection = undefined;
|
|
199
199
|
}
|
|
200
200
|
else if (request.id === TASK_RECOVER_ACTION) {
|
|
201
|
-
|
|
201
|
+
if (!recovery)
|
|
202
|
+
throw new Error(`task recover requires an active Troubleshooter recovery for ${task.id}`);
|
|
203
|
+
if (!recovery.executionId)
|
|
204
|
+
throw new Error(`Troubleshooter ${recovery.id} has no current execution for a fresh resolution`);
|
|
205
|
+
const proposal = { path: 'exact_original_retry', targetTaskId: task.id, recoveryExecutionId: recovery.executionId, rootCause: task.failureReason ?? task.error ?? 'durable failure', solution: `Recover exact original Task via ${request.recoveryTaskId}` };
|
|
202
206
|
const decision = dispatchTaskResolutionSync(task, proposal, 'troubleshooter', { activateRecovery: () => { applyTaskRetrySnapshot(task, { extendBudget: true }); recordRecoveryRouting(task, request); task.resolution = { ...materializeResolution(task, proposal, 'troubleshooter'), state: 'resolved' }; } });
|
|
203
207
|
if (!decision.accepted)
|
|
204
208
|
throw new Error(`Task resolution graph rejected exact-original recovery for ${task.id}`);
|
|
@@ -232,7 +236,9 @@ export async function dispatchTaskAction(store, request) {
|
|
|
232
236
|
const recovery = request.recoveryTaskId ? job.tasks.find(item => item.id === request.recoveryTaskId) : undefined;
|
|
233
237
|
if (!recovery || recovery.agentName !== 'troubleshooter' || recovery.parentTaskId !== task.id || !['QUEUED', 'RUNNING'].includes(recovery.state))
|
|
234
238
|
throw new Error(`task recover requires an active Troubleshooter recovery for ${task.id}`);
|
|
235
|
-
|
|
239
|
+
if (!recovery.executionId)
|
|
240
|
+
throw new Error(`Troubleshooter ${recovery.id} has no current execution for a fresh resolution`);
|
|
241
|
+
const proposal = { path: 'exact_original_retry', targetTaskId: task.id, recoveryExecutionId: recovery.executionId, rootCause: task.failureReason ?? task.error ?? 'durable failure', solution: `Recover exact original Task via ${request.recoveryTaskId}` };
|
|
236
242
|
const decision = dispatchTaskResolutionSync(task, proposal, 'troubleshooter', { activateRecovery: () => { applyTaskRetrySnapshot(task, { extendBudget: true }); recordRecoveryRouting(task, request); task.resolution = { ...materializeResolution(task, proposal, 'troubleshooter'), state: 'resolved' }; } });
|
|
237
243
|
if (!decision.accepted)
|
|
238
244
|
throw new Error(`Task resolution graph rejected exact-original recovery for ${task.id}`);
|
package/dist/task-recovery.js
CHANGED
|
@@ -52,7 +52,7 @@ export async function requestTroubleshooterEscalation(store, jobId, recoveryTask
|
|
|
52
52
|
if (!['QUEUED', 'RUNNING', 'FAILED'].includes(recovery.state))
|
|
53
53
|
throw new Error(`Troubleshooter ${recoveryTaskId} cannot escalate from ${recovery.state}`);
|
|
54
54
|
if (original) {
|
|
55
|
-
const proposal = { path: 'escalation_gate', targetTaskId: original.id, rootCause: request.context.trim(), solution: request.requiredAction.trim() };
|
|
55
|
+
const proposal = { path: 'escalation_gate', targetTaskId: original.id, recoveryExecutionId: recovery.executionId ?? undefined, rootCause: request.context.trim(), solution: request.requiredAction.trim() };
|
|
56
56
|
const decision = dispatchTaskResolutionSync(original, proposal, 'troubleshooter', {
|
|
57
57
|
createEscalationGate: () => { original.resolution = materializeResolution(original, proposal, 'troubleshooter', now); },
|
|
58
58
|
});
|
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'); }
|