agents-relay 2.0.81 → 2.0.82
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/README.md +3 -3
- package/dist/cli-help.js +5 -8
- package/dist/cli-review.js +1 -1
- package/dist/cli-support.js +2 -22
- package/dist/cli-worker-task.js +7 -6
- package/dist/cli.js +22 -59
- package/dist/dashboard-actions.js +2 -1
- package/dist/dashboard-html.js +5 -5
- package/dist/dashboard.js +0 -4
- package/dist/github-binding-order.js +1 -1
- package/dist/job-pr-number-migration.js +24 -0
- package/dist/job-task-id-sequence.js +16 -9
- package/dist/legacy-orchestrator-repair.js +51 -0
- package/dist/local-job-registry.js +2 -3
- package/dist/local-merge-delivery.js +1 -1
- package/dist/managed-worker-input.js +3 -4
- package/dist/planner.js +14 -8
- package/dist/pool.js +3 -1
- package/dist/reconciler-review.js +2 -2
- package/dist/reconciler.js +2 -8
- package/dist/relay-store.js +6 -4
- package/dist/relayd-control.js +10 -0
- package/dist/relayd-dashboard-actions.js +3 -33
- package/dist/relayd.js +17 -20
- package/dist/reviewer-thread.js +1 -1
- package/dist/runtime-status.js +2 -1
- package/dist/task-actions.js +47 -42
- package/dist/task-title.js +5 -0
- package/dist/troubleshooter.js +2 -2
- package/dist/worker-routing.js +1 -1
- package/dist/workflow/entity-creation.js +11 -1
- package/dist/workflow/orchestrator-rescue.js +62 -18
- package/dist/workflow/troubleshooter-v2.js +1 -1
- package/dist/workspace.js +5 -4
- package/package.json +1 -1
- package/skills/agents-relay/contracts/orchestrator-workflows.md +13 -12
- package/workflow/orchestrator-rescue.mmd +16 -1
- package/workflow/troubleshooter.mmd +4 -4
|
@@ -4,9 +4,8 @@ import { createReviewChairExecutionPlan } from './review-chair.js';
|
|
|
4
4
|
import { taskTagWorkerPrompt } from './task-tag.js';
|
|
5
5
|
import { executionEventSource } from './reconciler-runtime.js';
|
|
6
6
|
export async function managedWorkerInput(job, task, context) {
|
|
7
|
-
const isLocalJob = job.
|
|
8
|
-
const
|
|
9
|
-
const projectedReview = isLocalJob && task.agentName === 'reviewer' && externalPrNumber > 0;
|
|
7
|
+
const isLocalJob = Boolean(job.localMerge?.sourceRef);
|
|
8
|
+
const projectedReview = isLocalJob && task.agentName === 'reviewer' && job.prNumber > 0;
|
|
10
9
|
if (!['codex', 'chatgpt'].includes(task.adapter) || !job.repository)
|
|
11
10
|
return task.input;
|
|
12
11
|
const selectedAgent = task.agentName && context ? await agentContractPrompt(context, task.agentName) : '';
|
|
@@ -19,7 +18,7 @@ export async function managedWorkerInput(job, task, context) {
|
|
|
19
18
|
if (!task.executionId)
|
|
20
19
|
throw new Error(`Task ${task.id} requires an executionId before worker input is built`);
|
|
21
20
|
const sourceId = executionEventSource(job.id, task.id, task.executionId);
|
|
22
|
-
const prUrl =
|
|
21
|
+
const prUrl = job.prNumber > 0 ? `https://github.com/${job.repository}/pull/${job.prNumber}` : null;
|
|
23
22
|
const taskUrl = task.taskUrl ?? (isLocalJob ? `local:${job.id}/task/${task.id}` : `${prUrl}#issuecomment-${task.id}`);
|
|
24
23
|
const output = task.output.kind === 'file'
|
|
25
24
|
? `file\nPath: ${task.output.path}`
|
package/dist/planner.js
CHANGED
|
@@ -16,7 +16,7 @@ export function semanticTaskTree(tasks) {
|
|
|
16
16
|
const ids = new Set(work.map(task => task.id));
|
|
17
17
|
return {
|
|
18
18
|
roots: work.filter(task => !task.parentTaskId || !ids.has(task.parentTaskId)).map(task => task.id),
|
|
19
|
-
tasks: work.map(task => ({ id: task.id, parent_task_id: task.parentTaskId, state: task.state, dependencies: [...task.dependencies], planning_decision_id: task.planningDecisionId, planning_round: task.planningRound, review: task.review })),
|
|
19
|
+
tasks: work.map(task => ({ id: task.id, title: task.title, parent_task_id: task.parentTaskId, state: task.state, dependencies: [...task.dependencies], planning_decision_id: task.planningDecisionId, planning_round: task.planningRound, review: task.review })),
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
function plannerTrigger(job, manual = false) {
|
|
@@ -113,14 +113,14 @@ export function materializePlannerChild(job, planner, spec, executionContext) {
|
|
|
113
113
|
validateResolutionProposal(job, target, resolution);
|
|
114
114
|
}
|
|
115
115
|
const reviewerThread = spec.agentName === 'reviewer' ? prepareReviewerThreadDelivery(job, { id: spec.id, agentName: spec.agentName, reviewHeadSha }) : undefined;
|
|
116
|
-
return createTaskEntity(job, { jobId: job.id, id: spec.id, kind: 'work', priority: spec.priority ?? job.priority, storySize: spec.storySize ?? 'M', projectName: spec.projectName, agentName: spec.agentName, agentGraph: spec.agentGraph, review: spec.review, parentTaskId: spec.parentTaskId ?? null, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], workerRequirements: reviewerThread ? chatgptThreadRequirements(spec.workerRequirements) : spec.workerRequirements, testInjection: spec.testInjection, adapter: reviewerThread ? 'chatgpt' : 'codex', chatgptThreadDelivery: reviewerThread, input: spec.input, output: spec.output, continuation: spec.continuation ?? null, continuationDeliveredAt: null, attempt: 0, maxAttempts: spec.maxAttempts ?? 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: spec.timeoutMs ?? taskOverallTimeoutMs(spec.storySize ?? 'M'), createdAt: now, updatedAt: now, planningDecisionId: decisionId, planningSource: source, planningRound: round, reviewHeadSha, resolution: resolution ? { ...resolution, state: 'proposed', owner: 'planner', updatedAt: now } : undefined });
|
|
116
|
+
return createTaskEntity(job, { jobId: job.id, id: spec.id, title: spec.title, kind: 'work', priority: spec.priority ?? job.priority, storySize: spec.storySize ?? 'M', projectName: spec.projectName, agentName: spec.agentName, agentGraph: spec.agentGraph, review: spec.review, parentTaskId: spec.parentTaskId ?? null, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], workerRequirements: reviewerThread ? chatgptThreadRequirements(spec.workerRequirements) : spec.workerRequirements, testInjection: spec.testInjection, adapter: reviewerThread ? 'chatgpt' : 'codex', chatgptThreadDelivery: reviewerThread, input: spec.input, output: spec.output, continuation: spec.continuation ?? null, continuationDeliveredAt: null, attempt: 0, maxAttempts: spec.maxAttempts ?? 3, leaseOwner: null, leaseExpiresAt: null, executionId: null, threadId: null, result: null, plannerResult: null, error: null, timeoutMs: spec.timeoutMs ?? taskOverallTimeoutMs(spec.storySize ?? 'M'), createdAt: now, updatedAt: now, planningDecisionId: decisionId, planningSource: source, planningRound: round, reviewHeadSha, resolution: resolution ? { ...resolution, state: 'proposed', owner: 'planner', updatedAt: now } : undefined });
|
|
117
117
|
}
|
|
118
118
|
export function planningSpec(task) {
|
|
119
|
-
if (!task.output
|
|
119
|
+
if (!task.output)
|
|
120
120
|
throw new Error(`Initial task ${task.id} requires an output contract`);
|
|
121
|
-
if (!['codex', 'chatgpt'
|
|
122
|
-
throw new Error(`Initial task ${task.id} requires a supported adapter`);
|
|
123
|
-
return { id: task.id, parentTaskId: task.parentTaskId, input: task.input, priority: task.priority, storySize: task.storySize, projectName: task.projectName, agentName: task.agentName, agentGraph: task.agentGraph, review: task.review, dependencies: task.dependencies, capabilities: task.capabilities, workerRequirements: task.workerRequirements, testInjection: task.testInjection, adapter: task.adapter, output: task.output ?? { kind: 'task_pr' }, routing: task.routing, continuation: task.continuation, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs, resolution: task.resolution ? { ...task.resolution, state: undefined, owner: undefined, updatedAt: undefined } : undefined };
|
|
121
|
+
if (!['codex', 'chatgpt'].includes(task.adapter))
|
|
122
|
+
throw new Error(`Initial task ${task.id} requires a supported worker adapter`);
|
|
123
|
+
return { id: task.id, title: task.title, parentTaskId: task.parentTaskId, input: task.input, priority: task.priority, storySize: task.storySize, projectName: task.projectName, agentName: task.agentName, agentGraph: task.agentGraph, review: task.review, dependencies: task.dependencies, capabilities: task.capabilities, workerRequirements: task.workerRequirements, testInjection: task.testInjection, adapter: task.adapter, output: task.output ?? { kind: 'task_pr' }, routing: task.routing, continuation: task.continuation, maxAttempts: task.maxAttempts, timeoutMs: task.timeoutMs, resolution: task.resolution ? { ...task.resolution, state: undefined, owner: undefined, updatedAt: undefined } : undefined };
|
|
124
124
|
}
|
|
125
125
|
function record(value, field = 'result') {
|
|
126
126
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -309,7 +309,7 @@ export function parsePlannerResult(value) {
|
|
|
309
309
|
if (typeof timeoutMs !== 'number' || !Number.isFinite(timeoutMs) || timeoutMs < 0)
|
|
310
310
|
throw new Error(`Planner next_tasks[${index}].timeoutMs is invalid`);
|
|
311
311
|
return {
|
|
312
|
-
id, parentTaskId, input: task.input, priority, storySize,
|
|
312
|
+
id, title: optionalString(task.title, `next_tasks[${index}].title`), parentTaskId, input: task.input, priority, storySize,
|
|
313
313
|
projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'), agentGraph, review,
|
|
314
314
|
dependencies: stringArray(task.dependencies, 'dependencies'), capabilities: stringArray(task.capabilities, 'capabilities'), workerRequirements: requirements,
|
|
315
315
|
testInjection: parseTestInjection(task.testInjection ?? task['--test-injection']) ?? optionalString(task.testInjection ?? task['--test-injection'], `next_tasks[${index}].--test-injection`),
|
|
@@ -331,7 +331,7 @@ export function validatePlannerProgress(job, result) {
|
|
|
331
331
|
throw new Error('Planner returned in_progress with no executable tasks or unblocks while no live work remains; return satisfied when only Relay-owned review/merge/completion effects remain');
|
|
332
332
|
}
|
|
333
333
|
export function validatePlannerLifecycleOwnership(job, specs) {
|
|
334
|
-
if (
|
|
334
|
+
if (!job.localMerge?.sourceRef)
|
|
335
335
|
return;
|
|
336
336
|
for (const spec of specs) {
|
|
337
337
|
// Restrict ownership detection to an individual imperative clause. A long
|
|
@@ -347,6 +347,11 @@ export function validatePlannerLifecycleOwnership(job, specs) {
|
|
|
347
347
|
if (ownsLifecycle && [localMerge, jobCompletion, externalMerge].filter(Boolean).length >= 2) {
|
|
348
348
|
throw new Error(`Planner next task ${spec.id} attempts to own Graph lifecycle delivery; local merge, Job COMPLETED, and projected PR merge are Relay-owned effects and must not be worker Tasks`);
|
|
349
349
|
}
|
|
350
|
+
const releaseEffect = /\b(?:publish|release|deploy)\b.{0,100}\b(?:package|npm|service|runtime|version)\b|\b(?:package|npm|service|runtime)\b.{0,100}\b(?:publish|release|deploy)\b/.test(clause);
|
|
351
|
+
const releaseImperative = /^\s*(?:publish|release|deploy)\b|\b(?:then|and)\s+(?:publish|release|deploy)\b/.test(clause);
|
|
352
|
+
if (releaseEffect && releaseImperative) {
|
|
353
|
+
throw new Error(`Planner next task ${spec.id} attempts to own Relay release delivery; package publication and service/runtime deployment are Relay-owned effects and must not be worker Tasks`);
|
|
354
|
+
}
|
|
350
355
|
}
|
|
351
356
|
}
|
|
352
357
|
}
|
|
@@ -431,6 +436,7 @@ export function plannerInput(context) {
|
|
|
431
436
|
visible_agents: visibleAgents ?? [],
|
|
432
437
|
tasks: context.tasks.map(task => ({
|
|
433
438
|
id: task.id,
|
|
439
|
+
title: task.title,
|
|
434
440
|
kind: task.kind ?? 'work',
|
|
435
441
|
parent_task_id: task.parentTaskId,
|
|
436
442
|
state: task.state,
|
package/dist/pool.js
CHANGED
|
@@ -12,7 +12,9 @@ export function needsLifecycleReconciliation(job, now = Date.now()) {
|
|
|
12
12
|
// DRAFT create-only Jobs stay dormant until a caller explicitly creates work.
|
|
13
13
|
if (job.tasks.length === 0)
|
|
14
14
|
return false;
|
|
15
|
-
if (job.tasks.some(task => (task.state === 'QUEUED' && task.executionId === null)
|
|
15
|
+
if (job.tasks.some(task => (task.state === 'QUEUED' && task.executionId === null)
|
|
16
|
+
|| (task.state === 'RUNNING' && task.adapter === 'orchestrator' && task.executionId === null)
|
|
17
|
+
|| (task.state === 'DRAFT' && task.dependencies.length > 0)))
|
|
16
18
|
return true;
|
|
17
19
|
return job.tasks.some(task => ['RUNNING', 'QUEUED'].includes(task.state) && task.executionId !== null && task.leaseExpiresAt !== null && new Date(task.leaseExpiresAt).getTime() <= now);
|
|
18
20
|
}
|
|
@@ -122,7 +122,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
122
122
|
throw new Error('Too many final review rounds for ' + headSha);
|
|
123
123
|
}
|
|
124
124
|
externalReviewPrNumber(job) {
|
|
125
|
-
return job.
|
|
125
|
+
return job.prNumber;
|
|
126
126
|
}
|
|
127
127
|
async createFinalReviewTask(job, headSha) {
|
|
128
128
|
const id = this.nextFinalReviewTaskId(job, headSha);
|
|
@@ -163,7 +163,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
163
163
|
// Local-first Jobs may expose a review-only GitHub PR. Once such a PR is
|
|
164
164
|
// bound, its projected head must match the authoritative local head before
|
|
165
165
|
// a reviewer is asked to review that immutable PR identity.
|
|
166
|
-
if (!job.
|
|
166
|
+
if (!job.localMerge?.sourceRef || job.prNumber <= 0 || !this.store.pullRequestHead)
|
|
167
167
|
return true;
|
|
168
168
|
try {
|
|
169
169
|
const projected = await this.store.pullRequestHead();
|
package/dist/reconciler.js
CHANGED
|
@@ -28,7 +28,7 @@ export { managedWorkerInput } from './managed-worker-input.js';
|
|
|
28
28
|
import { recoverExpiredExecutions, recoverStartupOrphans as recoverStartupExecutionOrphans } from './execution-recovery.js';
|
|
29
29
|
import { dispatchJobReconcile } from './workflow/job-reconcile-v2.js';
|
|
30
30
|
import { reconcileLocalGitCompletion } from './local-merge-delivery.js';
|
|
31
|
-
import {
|
|
31
|
+
import { repairLegacyOrchestrators } from './legacy-orchestrator-repair.js';
|
|
32
32
|
import { launchPlanExecution } from './planner-runtime.js';
|
|
33
33
|
import { classifyFailure, errorMessage, observability, withObservabilityCorrelation } from './observability.js';
|
|
34
34
|
const PRIORITY_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
@@ -93,13 +93,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
93
93
|
if (job.state !== 'ACTIVE')
|
|
94
94
|
return job;
|
|
95
95
|
await recoverExpiredExecutions(job, now, this.executionRecoveryPorts(job));
|
|
96
|
-
|
|
97
|
-
const rescue = applyOrchestratorRescue(task, now);
|
|
98
|
-
if (!rescue.rescued)
|
|
99
|
-
continue;
|
|
100
|
-
await this.store.saveTask(task);
|
|
101
|
-
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'task.orchestrator_rescued', 'queued', `Stale orchestrator task ${task.id} converted to ChatGPT execution`, 'orchestrator', { ageMs: rescue.ageMs, adapter: 'chatgpt' }, { agent: 'relay', component: 'agents-relay' }));
|
|
102
|
-
}
|
|
96
|
+
await repairLegacyOrchestrators(jobId, now, { store: this.store, owner: this.options.owner, emit: event => this.emit(event) });
|
|
103
97
|
await this.recoverPlannerChildren(job);
|
|
104
98
|
await retryPendingPlannerUnblocks({
|
|
105
99
|
store: this.store,
|
package/dist/relay-store.js
CHANGED
|
@@ -4,6 +4,7 @@ import { mkdirSync } from 'node:fs';
|
|
|
4
4
|
import { readdir } from 'node:fs/promises';
|
|
5
5
|
import { recordPublicationInDatabase, loadPublicationFromDatabase, listPublicationEventsFromDatabase } from './publication-store.js';
|
|
6
6
|
import { observability } from './observability.js';
|
|
7
|
+
import { migrateLegacyJobPrNumbers, normalizeLegacyJobPrNumber } from './job-pr-number-migration.js';
|
|
7
8
|
import { ProjectionOutbox, SCHEMA_VERSION, logicalOperationKey, } from './projection-outbox.js';
|
|
8
9
|
function newerRecord(local, remote) {
|
|
9
10
|
if (!local)
|
|
@@ -99,6 +100,7 @@ export class SQLiteRelayStore {
|
|
|
99
100
|
${ProjectionOutbox.createTableSql()}
|
|
100
101
|
${ProjectionOutbox.createIndexSql()}
|
|
101
102
|
`);
|
|
103
|
+
migrateLegacyJobPrNumbers(this.db);
|
|
102
104
|
const current = this.db.prepare("SELECT value FROM relay_meta WHERE key='schema_version'").get();
|
|
103
105
|
const version = current?.value ? Number(current.value) : 0;
|
|
104
106
|
if (version > SCHEMA_VERSION)
|
|
@@ -119,11 +121,11 @@ export class SQLiteRelayStore {
|
|
|
119
121
|
}
|
|
120
122
|
async loadJob(jobId) {
|
|
121
123
|
const row = this.db.prepare('SELECT json FROM job_snapshot WHERE job_id=?').get(jobId);
|
|
122
|
-
return row?.json ? JSON.parse(row.json) : null;
|
|
124
|
+
return row?.json ? normalizeLegacyJobPrNumber(JSON.parse(row.json)) : null;
|
|
123
125
|
}
|
|
124
126
|
async listJobs() {
|
|
125
127
|
const rows = this.db.prepare('SELECT json FROM job_snapshot ORDER BY job_id').all();
|
|
126
|
-
return rows.map(row => JSON.parse(row.json));
|
|
128
|
+
return rows.map(row => normalizeLegacyJobPrNumber(JSON.parse(row.json)));
|
|
127
129
|
}
|
|
128
130
|
async saveJob(job) {
|
|
129
131
|
this.writeJobSnapshot(job, new Date().toISOString());
|
|
@@ -135,7 +137,7 @@ export class SQLiteRelayStore {
|
|
|
135
137
|
const row = this.db.prepare('SELECT json FROM job_snapshot WHERE job_id=?').get(jobId);
|
|
136
138
|
if (!row?.json)
|
|
137
139
|
throw new Error(`Local authoritative Job ${jobId} not found`);
|
|
138
|
-
const current = JSON.parse(row.json);
|
|
140
|
+
const current = normalizeLegacyJobPrNumber(JSON.parse(row.json));
|
|
139
141
|
const before = JSON.stringify(current);
|
|
140
142
|
const next = mutation(current);
|
|
141
143
|
if (next.id !== jobId)
|
|
@@ -388,7 +390,7 @@ export async function discoverLocalRelayStoreSnapshots(directory) {
|
|
|
388
390
|
try {
|
|
389
391
|
db = new DatabaseSync(path, { readOnly: true });
|
|
390
392
|
const rows = db.prepare('SELECT json FROM job_snapshot ORDER BY job_id').all();
|
|
391
|
-
const jobs = rows.map(row => JSON.parse(row.json));
|
|
393
|
+
const jobs = rows.map(row => normalizeLegacyJobPrNumber(JSON.parse(row.json)));
|
|
392
394
|
const row = db.prepare('SELECT state,head_ref,head_sha,observed_at FROM pull_request_facts WHERE id=1').get();
|
|
393
395
|
const facts = row ? {
|
|
394
396
|
state: row.state,
|
package/dist/relayd-control.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeThreadId } from './reviewer-thread.js';
|
|
2
|
+
import { supersedeCancelledTask } from './task-actions.js';
|
|
2
3
|
export function createWorkspaceControl(deps) {
|
|
3
4
|
const { client, trusted, noGitHub, repositories, refreshAll, requireLocalJobBinding, makeReconciler, concurrency, pool, bus, isEffectivelyBlocked, unblockAutonomousTasks, unblockTask, publishTaskUnblocked, dispatchTaskAction, registerBlockerDependency, discoverAgents, ensureManagedGitHubJob } = deps;
|
|
4
5
|
const workspaceControl = async (body) => {
|
|
@@ -106,6 +107,15 @@ export function createWorkspaceControl(deps) {
|
|
|
106
107
|
await pool.reconcileLocalJob(jobId, 'control.job.unblock');
|
|
107
108
|
return await store.load(jobId);
|
|
108
109
|
}
|
|
110
|
+
if (command === 'task.supersede') {
|
|
111
|
+
const taskId = typeof body.taskId === 'string' ? body.taskId : '';
|
|
112
|
+
const reason = typeof body.reason === 'string' ? body.reason : '';
|
|
113
|
+
if (!taskId || !reason.trim())
|
|
114
|
+
throw new Error('task supersede requires taskId and reason');
|
|
115
|
+
await supersedeCancelledTask(store, jobId, taskId, reason);
|
|
116
|
+
await pool.reconcileLocalJob(jobId, 'control.task.supersede');
|
|
117
|
+
return await store.load(jobId);
|
|
118
|
+
}
|
|
109
119
|
if (command === 'task.recover') {
|
|
110
120
|
const taskId = typeof body.taskId === 'string' ? body.taskId : '';
|
|
111
121
|
const recoveryTaskId = typeof body.recoveryTaskId === 'string' ? body.recoveryTaskId : '';
|
|
@@ -3,11 +3,11 @@ import { allocateEntityId, isGeneratedEntityId } from './job-task-id-sequence.js
|
|
|
3
3
|
import { relayStoreDirectory } from './relay-store.js';
|
|
4
4
|
import { dispatchJobLifecycleSync } from './workflow/lifecycle.js';
|
|
5
5
|
export function createDashboardActions(deps) {
|
|
6
|
-
const { getOverviews, requireLocalJobBinding, pool, dashboardJobAction, makeReconciler, concurrency, dashboardJobSetState, refreshAll, applyJobActivateAction,
|
|
6
|
+
const { getOverviews, requireLocalJobBinding, pool, dashboardJobAction, makeReconciler, concurrency, dashboardJobSetState, refreshAll, applyJobActivateAction, bus, dashboardTaskAction, dashboardTaskSetState, storeDirectory } = deps;
|
|
7
7
|
const currentOverviews = () => getOverviews();
|
|
8
8
|
const dashboardOverview = (jobId, target) => currentOverviews().find((item) => item.job.id === jobId
|
|
9
9
|
&& (!target?.repository || item.job.repository === target.repository)
|
|
10
|
-
&& (!target?.prNumber || (item.job.
|
|
10
|
+
&& (!target?.prNumber || (item.job.prNumber) === target.prNumber));
|
|
11
11
|
const workspaceJobAction = (action) => async (jobId) => {
|
|
12
12
|
const overview = currentOverviews().find((item) => item.job.id === jobId);
|
|
13
13
|
if (!overview)
|
|
@@ -84,36 +84,6 @@ export function createDashboardActions(deps) {
|
|
|
84
84
|
await pool.reconcileLocalJob(jobId, 'control.task.create');
|
|
85
85
|
return { ...(await store.load(jobId)), createdTaskId: task.id };
|
|
86
86
|
};
|
|
87
|
-
const workspaceTaskStart = async (jobId, taskId) => {
|
|
88
|
-
const overview = currentOverviews().find((item) => item.job.id === jobId);
|
|
89
|
-
if (!overview)
|
|
90
|
-
throw new Error(`Managed job ${jobId} not found`);
|
|
91
|
-
const store = (await requireLocalJobBinding(jobId)).store;
|
|
92
|
-
const job = await store.load(jobId);
|
|
93
|
-
const task = job.tasks.find((item) => item.id === taskId);
|
|
94
|
-
if (!task)
|
|
95
|
-
throw new Error(`Task ${taskId} not found`);
|
|
96
|
-
await applyOrchestratorStartAction(task);
|
|
97
|
-
await store.saveTask(task);
|
|
98
|
-
await publishOrchestratorTaskStarted(bus, job, task);
|
|
99
|
-
await pool.reconcileLocalJob(jobId, 'control.task.start');
|
|
100
|
-
return await store.load(jobId);
|
|
101
|
-
};
|
|
102
|
-
const workspaceTaskComplete = async (jobId, taskId, summary) => {
|
|
103
|
-
const overview = currentOverviews().find((item) => item.job.id === jobId);
|
|
104
|
-
if (!overview)
|
|
105
|
-
throw new Error(`Managed job ${jobId} not found`);
|
|
106
|
-
const store = (await requireLocalJobBinding(jobId)).store;
|
|
107
|
-
const job = await store.load(jobId);
|
|
108
|
-
const task = job.tasks.find((item) => item.id === taskId);
|
|
109
|
-
if (!task)
|
|
110
|
-
throw new Error(`Task ${taskId} not found`);
|
|
111
|
-
await applyOrchestratorCompleteAction(task, summary);
|
|
112
|
-
await store.saveTask(task);
|
|
113
|
-
await publishOrchestratorTaskCompletion(bus, job, task, summary);
|
|
114
|
-
await pool.reconcileLocalJob(jobId, 'control.task.complete');
|
|
115
|
-
return await store.load(jobId);
|
|
116
|
-
};
|
|
117
87
|
const workspaceTaskAction = (action) => async (jobId, taskId) => {
|
|
118
88
|
const overview = currentOverviews().find((item) => item.job.id === jobId);
|
|
119
89
|
if (!overview)
|
|
@@ -130,5 +100,5 @@ export function createDashboardActions(deps) {
|
|
|
130
100
|
await refreshAll();
|
|
131
101
|
return result;
|
|
132
102
|
};
|
|
133
|
-
return { dashboardOverview, workspaceJobAction, workspaceJobSetState, workspaceTaskCreate,
|
|
103
|
+
return { dashboardOverview, workspaceJobAction, workspaceJobSetState, workspaceTaskCreate, workspaceTaskAction, workspaceTaskSetState };
|
|
134
104
|
}
|
package/dist/relayd.js
CHANGED
|
@@ -24,14 +24,13 @@ import { githubAuthContext } from './github-auth.js';
|
|
|
24
24
|
import { DisabledGitHubClient, noGitHubMode } from './no-github.js';
|
|
25
25
|
import { basename } from 'node:path';
|
|
26
26
|
import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
|
|
27
|
-
import { eventBusResolutionMessage,
|
|
27
|
+
import { eventBusResolutionMessage, publishTaskUnblocked, resolveEventBus } from './cli-support.js';
|
|
28
28
|
import { eventFor } from './events.js';
|
|
29
29
|
import { dispatchTaskAction } from './task-actions.js';
|
|
30
30
|
import { isEffectivelyBlocked, unblockAutonomousTasks, unblockTask } from './unblock.js';
|
|
31
31
|
import { registerBlockerDependency } from './blockers.js';
|
|
32
32
|
import { ensureManagedGitHubJob } from './cli-job-create.js';
|
|
33
33
|
import { applyJobActivateAction } from './job-actions.js';
|
|
34
|
-
import { applyOrchestratorStartAction, applyOrchestratorCompleteAction } from './task-actions.js';
|
|
35
34
|
import { managedTaskUpdateHandler } from './task-update.js';
|
|
36
35
|
import { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
|
|
37
36
|
import { codexAndZaiUsageRegistry } from './usage.js';
|
|
@@ -155,7 +154,7 @@ export async function runDaemon(argv) {
|
|
|
155
154
|
cachedOverviews = await inventoryFromLocal();
|
|
156
155
|
const makeReconciler = (store, maxConcurrent) => new Reconciler(store, { noGitHub, owner: `relayd-${process.pid}`, maxConcurrent, leaseMs: Number(value(argv, '--lease-ms', '300000')), adapters: [new CodexAdapter(value(argv, '--codex', 'codex')), new ChatGptAdapter()], continuations: [new CodexThreadContinuation(value(argv, '--codex', 'codex')), new CommandContinuation(), new WebhookContinuation()], planner: runtimePlanner(argv), usageSnapshotProvider: () => usageRegistry.snapshot(), runtimeSummaryProvider: () => ({ chatgptRunning: summarizeRuntime(cachedOverviews.map(item => item.job)).workers.chatgpt }), decisionEngine: executionDecisionEngine(), decisionMode: decisionMode(), decisionAuditStore, eventBus: bus, reviewer: runtimeReviewer(argv), projectPathForJob: async (job) => {
|
|
157
156
|
const canonical = repositoryPaths.get(job.repository) ?? await resolveManagedProjectPath(job.repository, explicitProjectPath, workspaceRoot);
|
|
158
|
-
if (job.
|
|
157
|
+
if (!job.localMerge?.sourceRef || job.schedule)
|
|
159
158
|
return resolveRepositoryProjectPath(canonical, job.projectPath);
|
|
160
159
|
const sourceRef = await ensureLocalJobSourceBranch(canonical, job);
|
|
161
160
|
if (!store.mutateJob)
|
|
@@ -243,15 +242,15 @@ export async function runDaemon(argv) {
|
|
|
243
242
|
// A completed local Job may never have had a display PR. Publish main
|
|
244
243
|
// regardless: GitHub availability cannot determine SQLite completion.
|
|
245
244
|
if (discovered.state === 'COMPLETED' && discovered.localMerge?.status === 'succeeded'
|
|
246
|
-
&&
|
|
245
|
+
&& discovered.prNumber <= 0) {
|
|
247
246
|
const published = projectAuthoritativeLocalMerge(canonical, discovered);
|
|
248
247
|
if (published.status === 'projected')
|
|
249
248
|
await bus?.publish(eventFor(discovered.id, 'projection', null, 'projection.delivered', 'succeeded', `GitHub main synchronization ${published.status}`, 'orchestrator', { operation: 'github.syncMain', deliveryStatus: published.status, mergedCommitSha: published.localMergedCommitSha }, { agent: 'relay', component: 'github-projection' }));
|
|
250
249
|
continue;
|
|
251
250
|
}
|
|
252
|
-
const
|
|
251
|
+
const existingPrNumber = discovered.prNumber > 0 ? discovered.prNumber : 0;
|
|
253
252
|
const terminal = ['COMPLETED', 'CANCELLED'].includes(discovered.state);
|
|
254
|
-
if (terminal &&
|
|
253
|
+
if (terminal && existingPrNumber <= 0)
|
|
255
254
|
continue;
|
|
256
255
|
const sourceSha = await gitText(canonical, ['rev-parse', '--verify', sourceRef]).catch(() => '');
|
|
257
256
|
if (!sourceSha)
|
|
@@ -275,10 +274,10 @@ export async function runDaemon(argv) {
|
|
|
275
274
|
}));
|
|
276
275
|
}
|
|
277
276
|
let job = (await relay.loadJob(discovered.id)) ?? discovered;
|
|
278
|
-
let
|
|
279
|
-
if (
|
|
277
|
+
let prNumber = job.prNumber > 0 ? job.prNumber : 0;
|
|
278
|
+
if (prNumber === 0 && ['COMPLETED', 'CANCELLED'].includes(job.state))
|
|
280
279
|
continue;
|
|
281
|
-
if (
|
|
280
|
+
if (prNumber === 0) {
|
|
282
281
|
// Do not bootstrap an empty remote-only PR commit: it would diverge from
|
|
283
282
|
// the real local Job branch before its first Worker commit. Publish the
|
|
284
283
|
// review-only PR when there is an actual diff against main.
|
|
@@ -295,17 +294,17 @@ export async function runDaemon(argv) {
|
|
|
295
294
|
const pr = await resolvePullRequest(client, job.repository, { head: branch, base, title: job.title, body: job.objective ?? job.description ?? '' });
|
|
296
295
|
if (!relay.mutateJob)
|
|
297
296
|
throw new Error('Atomic SQLite mutation is required for GitHub PR binding');
|
|
298
|
-
job = await relay.mutateJob(job.id, current => ({ ...current,
|
|
299
|
-
|
|
297
|
+
job = await relay.mutateJob(job.id, current => ({ ...current, prNumber: pr.number, updatedAt: new Date().toISOString() }));
|
|
298
|
+
prNumber = pr.number;
|
|
300
299
|
observability().record({ stage: 'projection', event: 'github.pr_bound', outcome: 'succeeded', message: `Bound local Job ${job.id} to GitHub PR #${pr.number}`, correlation: { repository: job.repository, prNumber: pr.number, jobId: job.id }, data: { head: branch, base } });
|
|
301
300
|
}
|
|
302
|
-
if (
|
|
301
|
+
if (prNumber <= 0)
|
|
303
302
|
continue;
|
|
304
303
|
const latestFacts = await relay.loadPullRequestFacts();
|
|
305
304
|
if (latestFacts?.state !== 'MERGED' && (latestFacts?.head?.sha !== sourceSha || latestFacts?.head?.ref !== branch)) {
|
|
306
305
|
await relay.savePullRequestFacts({ state: latestFacts?.state === 'CLOSED' ? 'CLOSED' : 'OPEN', head: { ref: branch, sha: sourceSha }, observedAt: new Date().toISOString() });
|
|
307
306
|
}
|
|
308
|
-
const github = new GitHubStore(client, job.repository,
|
|
307
|
+
const github = new GitHubStore(client, job.repository, prNumber, trusted);
|
|
309
308
|
projectionWriters.set(`job:${job.id}`, new GitHubProjectionWriter(relay, github, undefined, true));
|
|
310
309
|
if (job.state === 'COMPLETED' && job.localMerge?.status === 'succeeded' && job.localMerge.projectionStrategy === 'target_ref_push') {
|
|
311
310
|
const injection = await consumeMethodInjection(relay, job.repository, job.id, 'github.projectLocalMerge');
|
|
@@ -314,14 +313,14 @@ export async function runDaemon(argv) {
|
|
|
314
313
|
// A review-only PR is never merged by GitHub. Close it after local main
|
|
315
314
|
// has been safely published, retaining the diff, comments and commit link.
|
|
316
315
|
const remotePr = await github.pullRequestState();
|
|
317
|
-
await archiveDisplayPullRequest(client, job.repository,
|
|
316
|
+
await archiveDisplayPullRequest(client, job.repository, prNumber, job.id, projectedMerge.projectedCommitSha, remotePr);
|
|
318
317
|
const projectedState = await github.pullRequestState();
|
|
319
318
|
const projectedHead = await github.pullRequestHead();
|
|
320
319
|
await relay.savePullRequestFacts({ state: projectedState, head: projectedHead, observedAt: new Date().toISOString() });
|
|
321
320
|
observability().record({
|
|
322
321
|
stage: 'projection', event: 'github.local_merge_projected', outcome: 'succeeded',
|
|
323
322
|
message: `Projected authoritative local merge ${projectedMerge.localMergedCommitSha} to ${projectedMerge.targetRef}`,
|
|
324
|
-
correlation: { repository: job.repository, prNumber
|
|
323
|
+
correlation: { repository: job.repository, prNumber, jobId: job.id },
|
|
325
324
|
data: { ...projectedMerge, githubState: projectedState },
|
|
326
325
|
});
|
|
327
326
|
}
|
|
@@ -334,7 +333,7 @@ export async function runDaemon(argv) {
|
|
|
334
333
|
observability().record({
|
|
335
334
|
stage: 'projection', event: 'github.binding_job_failed', level: 'warn', outcome: 'failed',
|
|
336
335
|
message: `GitHub binding/projection failed for ${discovered.id}: ${reason}`,
|
|
337
|
-
correlation: { repository: discovered.repository, prNumber: discovered.
|
|
336
|
+
correlation: { repository: discovered.repository, prNumber: discovered.prNumber, jobId: discovered.id },
|
|
338
337
|
data: { classification, reason },
|
|
339
338
|
});
|
|
340
339
|
}
|
|
@@ -362,7 +361,7 @@ export async function runDaemon(argv) {
|
|
|
362
361
|
const initial = cachedOverviews;
|
|
363
362
|
const fallback = initial[0]?.job;
|
|
364
363
|
const dashboardStore = fallback ? (await requireLocalJobBinding(fallback.id)).store : new InMemoryStore();
|
|
365
|
-
const { dashboardOverview, workspaceJobAction, workspaceJobSetState, workspaceTaskCreate,
|
|
364
|
+
const { dashboardOverview, workspaceJobAction, workspaceJobSetState, workspaceTaskCreate, workspaceTaskAction, workspaceTaskSetState } = createDashboardActions({ getOverviews: () => cachedOverviews, requireLocalJobBinding, pool, dashboardJobAction, makeReconciler, concurrency, dashboardJobSetState, refreshAll, applyJobActivateAction, bus, dashboardTaskAction, dashboardTaskSetState });
|
|
366
365
|
const workspaceControl = createWorkspaceControl({ client, trusted, noGitHub, repositories, refreshAll, requireLocalJobBinding, makeReconciler, concurrency, pool, bus, isEffectivelyBlocked, unblockAutonomousTasks, unblockTask, publishTaskUnblocked, dispatchTaskAction, registerBlockerDependency, discoverAgents, ensureManagedGitHubJob });
|
|
367
366
|
const secretFile = value(argv, '--github-webhook-secret-file');
|
|
368
367
|
const secret = noGitHub ? '' : value(argv, '--github-webhook-secret') || process.env.AGENTS_RELAY_GITHUB_WEBHOOK_SECRET || (secretFile ? (await readFile(secretFile, 'utf8')).trim() : '');
|
|
@@ -370,7 +369,7 @@ export async function runDaemon(argv) {
|
|
|
370
369
|
secret,
|
|
371
370
|
repositories: new Set(repositories),
|
|
372
371
|
wake: async (event) => {
|
|
373
|
-
const targets = cachedOverviews.filter(item => item.job.repository === event.repository && (event.jobId ? item.job.id === event.jobId :
|
|
372
|
+
const targets = cachedOverviews.filter(item => item.job.repository === event.repository && (event.jobId ? item.job.id === event.jobId : item.job.prNumber === event.pullRequest));
|
|
374
373
|
if (targets.length === 0)
|
|
375
374
|
return;
|
|
376
375
|
for (const target of targets) {
|
|
@@ -429,10 +428,8 @@ export async function runDaemon(argv) {
|
|
|
429
428
|
const binding = await requireLocalJobBinding(jobId);
|
|
430
429
|
return { store: binding.store, repository: binding.job.repository, prNumber: binding.job.prNumber };
|
|
431
430
|
}, async (_repository, _prNumber, jobId) => pool.reconcileLocalJob(jobId, 'control.task.update')),
|
|
432
|
-
taskStart: workspaceTaskStart,
|
|
433
431
|
taskRetry: workspaceTaskAction('retry'),
|
|
434
432
|
taskCancel: workspaceTaskAction('cancel'),
|
|
435
|
-
taskComplete: workspaceTaskComplete,
|
|
436
433
|
taskSetState: workspaceTaskSetState,
|
|
437
434
|
}, async ({ jobId, taskId }) => {
|
|
438
435
|
const overview = cachedOverviews.find(item => item.job.id === jobId);
|
package/dist/reviewer-thread.js
CHANGED
|
@@ -42,7 +42,7 @@ export function prepareReviewerThreadDelivery(job, task, override) {
|
|
|
42
42
|
return {
|
|
43
43
|
kind: 'new-turn', threadId: destination.threadId, threadSource: destination.source,
|
|
44
44
|
requestId: existing?.requestId ?? `${job.id}:${task.id}:${task.reviewHeadSha ?? job.headSha ?? 'review'}`,
|
|
45
|
-
jobId: job.id, prNumber: job.
|
|
45
|
+
jobId: job.id, prNumber: job.prNumber,
|
|
46
46
|
...(task.reviewHeadSha ? { reviewHeadSha: task.reviewHeadSha } : {}),
|
|
47
47
|
projectId: task.routing?.projectId ?? null,
|
|
48
48
|
...(existing?.executionId ? { executionId: existing.executionId } : {}),
|
package/dist/runtime-status.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
function isSuperseded(task) {
|
|
2
|
-
return task.state === 'CANCELLED' &&
|
|
2
|
+
return task.state === 'CANCELLED' && ((task.resolution?.path === 'cancel_supersede' && task.resolution.state === 'resolved')
|
|
3
|
+
|| (typeof task.error === 'string' && task.error.startsWith('Superseded ')));
|
|
3
4
|
}
|
|
4
5
|
export function implementationTasks(job) {
|
|
5
6
|
return job.tasks.filter(task => task.kind !== 'planner'
|
package/dist/task-actions.js
CHANGED
|
@@ -7,8 +7,6 @@ export const TASK_UNBLOCK_RETRY_ACTION = 'task.unblock.retry';
|
|
|
7
7
|
export const TASK_CANCEL_ACTION = 'task.cancel';
|
|
8
8
|
export const TASK_RELEASE_ACTION = 'task.release';
|
|
9
9
|
export const TASK_HOLD_ACTION = 'task.hold';
|
|
10
|
-
export const TASK_START_ACTION = 'task.start';
|
|
11
|
-
export const TASK_COMPLETE_ACTION = 'task.complete';
|
|
12
10
|
export const TASK_QUEUE_ACTION = 'task.queue';
|
|
13
11
|
function lifecycleRejection(task, action) {
|
|
14
12
|
return new Error(`Task ${task.id} is not ${action} (${task.state})`);
|
|
@@ -100,42 +98,6 @@ export async function applyTaskQueueAction(job, task) {
|
|
|
100
98
|
clearExecution(task);
|
|
101
99
|
return task;
|
|
102
100
|
}
|
|
103
|
-
export async function applyOrchestratorStartAction(task) {
|
|
104
|
-
if (task.adapter !== 'orchestrator')
|
|
105
|
-
throw new Error('task start is only valid for adapter=orchestrator self-tasks');
|
|
106
|
-
const originalState = task.state;
|
|
107
|
-
try {
|
|
108
|
-
await transitionTaskEvent(task, 'task.started');
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
if (error instanceof Error && error.message.startsWith('No enabled workflow transition')) {
|
|
112
|
-
throw new Error(`Task ${task.id} is ${originalState}; only QUEUED orchestrator tasks can be started`);
|
|
113
|
-
}
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
task.attempt = Math.max(task.attempt, 1);
|
|
117
|
-
task.updatedAt = new Date().toISOString();
|
|
118
|
-
return task;
|
|
119
|
-
}
|
|
120
|
-
export async function applyOrchestratorCompleteAction(task, summary) {
|
|
121
|
-
if (task.adapter !== 'orchestrator')
|
|
122
|
-
throw new Error('task complete is only valid for adapter=orchestrator self-tasks');
|
|
123
|
-
const originalState = task.state;
|
|
124
|
-
if (task.state !== 'RUNNING')
|
|
125
|
-
throw new Error(`Task ${task.id} is ${originalState}; orchestrator task must be started before completion`);
|
|
126
|
-
task.result = { summary };
|
|
127
|
-
clearExecution(task);
|
|
128
|
-
try {
|
|
129
|
-
await transitionTaskEvent(task, 'task.completed');
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
if (error instanceof Error && error.message.startsWith('No enabled workflow transition')) {
|
|
133
|
-
throw new Error(`Task ${task.id} is ${originalState}; only RUNNING orchestrator tasks can be completed`);
|
|
134
|
-
}
|
|
135
|
-
throw error;
|
|
136
|
-
}
|
|
137
|
-
return task;
|
|
138
|
-
}
|
|
139
101
|
async function applySelectedRelatedTaskMutations(job, mutations) {
|
|
140
102
|
const changed = [];
|
|
141
103
|
for (const mutation of mutations) {
|
|
@@ -148,6 +110,45 @@ async function applySelectedRelatedTaskMutations(job, mutations) {
|
|
|
148
110
|
}
|
|
149
111
|
return changed;
|
|
150
112
|
}
|
|
113
|
+
function markCancelledTaskSuperseded(task, reason) {
|
|
114
|
+
if (task.kind === 'planner')
|
|
115
|
+
throw new Error(`Legacy planner Task ${task.id} is read-only`);
|
|
116
|
+
if (task.state !== 'CANCELLED')
|
|
117
|
+
throw new Error(`Task ${task.id} must already be CANCELLED before supersede annotation`);
|
|
118
|
+
if (!reason.trim())
|
|
119
|
+
throw new Error('task supersede requires a reason');
|
|
120
|
+
const proposal = { path: 'cancel_supersede', targetTaskId: task.id, rootCause: reason.trim(), solution: 'Superseded by later corrected durable work' };
|
|
121
|
+
const decision = dispatchTaskResolutionSync(task, proposal, 'external', { cancelSupersede: () => {
|
|
122
|
+
task.resolution = { ...materializeResolution(task, proposal, 'external'), state: 'resolved' };
|
|
123
|
+
task.error = `Superseded ${reason.trim()}`;
|
|
124
|
+
task.updatedAt = new Date().toISOString();
|
|
125
|
+
} });
|
|
126
|
+
if (!decision.accepted)
|
|
127
|
+
throw new Error(`Task resolution graph rejected cancellation supersede for ${task.id}`);
|
|
128
|
+
}
|
|
129
|
+
export async function supersedeCancelledTask(store, jobId, taskId, reason) {
|
|
130
|
+
if (store.mutateJob) {
|
|
131
|
+
let updatedTask;
|
|
132
|
+
const updated = await store.mutateJob(jobId, current => {
|
|
133
|
+
const task = current.tasks.find(item => item.id === taskId);
|
|
134
|
+
if (!task)
|
|
135
|
+
throw new Error(`Task ${taskId} not found`);
|
|
136
|
+
markCancelledTaskSuperseded(task, reason);
|
|
137
|
+
updatedTask = structuredClone(task);
|
|
138
|
+
return current;
|
|
139
|
+
});
|
|
140
|
+
if (!updatedTask)
|
|
141
|
+
throw new Error(`Task ${taskId} was not updated`);
|
|
142
|
+
return { job: updated, task: updatedTask };
|
|
143
|
+
}
|
|
144
|
+
const job = await store.load(jobId);
|
|
145
|
+
const task = job.tasks.find(item => item.id === taskId);
|
|
146
|
+
if (!task)
|
|
147
|
+
throw new Error(`Task ${taskId} not found`);
|
|
148
|
+
markCancelledTaskSuperseded(task, reason);
|
|
149
|
+
await store.saveTask(task);
|
|
150
|
+
return { job: await store.load(jobId), task };
|
|
151
|
+
}
|
|
151
152
|
/** Compatibility bridge: domain mutation only; adapters own wake/reconcile/output behavior. */
|
|
152
153
|
export async function dispatchTaskAction(store, request) {
|
|
153
154
|
if (store.mutateJob && [TASK_RETRY_ACTION, TASK_RECOVER_ACTION, TASK_CANCEL_ACTION].includes(request.id)) {
|
|
@@ -162,8 +163,11 @@ export async function dispatchTaskAction(store, request) {
|
|
|
162
163
|
throw new Error(`task recover requires an active Troubleshooter recovery for ${task.id}`);
|
|
163
164
|
}
|
|
164
165
|
const relatedMutations = request.id === TASK_RECOVER_ACTION ? [] : selectRelatedTaskMutations(current, task, request.id);
|
|
165
|
-
if (request.id === TASK_RETRY_ACTION)
|
|
166
|
+
if (request.id === TASK_RETRY_ACTION) {
|
|
166
167
|
applyTaskRetrySnapshot(task, { extendBudget: true });
|
|
168
|
+
task.routing = undefined;
|
|
169
|
+
task.workerSelection = undefined;
|
|
170
|
+
}
|
|
167
171
|
else if (request.id === TASK_RECOVER_ACTION) {
|
|
168
172
|
const proposal = { path: 'exact_original_retry', targetTaskId: task.id, rootCause: task.failureReason ?? task.error ?? 'durable failure', solution: `Recover exact original Task via ${request.recoveryTaskId}` };
|
|
169
173
|
const decision = dispatchTaskResolutionSync(task, proposal, 'troubleshooter', { activateRecovery: () => { applyTaskRetrySnapshot(task, { extendBudget: true }); task.resolution = { ...materializeResolution(task, proposal, 'troubleshooter'), state: 'resolved' }; } });
|
|
@@ -190,8 +194,11 @@ export async function dispatchTaskAction(store, request) {
|
|
|
190
194
|
if (!task)
|
|
191
195
|
throw new Error(`Task ${request.taskId} not found`);
|
|
192
196
|
const relatedMutations = request.id === TASK_RECOVER_ACTION ? [] : selectRelatedTaskMutations(job, task, request.id);
|
|
193
|
-
if (request.id === TASK_RETRY_ACTION)
|
|
197
|
+
if (request.id === TASK_RETRY_ACTION) {
|
|
194
198
|
await applyTaskRetryAction(task, { extendBudget: true });
|
|
199
|
+
task.routing = undefined;
|
|
200
|
+
task.workerSelection = undefined;
|
|
201
|
+
}
|
|
195
202
|
else if (request.id === TASK_RECOVER_ACTION) {
|
|
196
203
|
const recovery = request.recoveryTaskId ? job.tasks.find(item => item.id === request.recoveryTaskId) : undefined;
|
|
197
204
|
if (!recovery || recovery.agentName !== 'troubleshooter' || recovery.parentTaskId !== task.id || !['QUEUED', 'RUNNING'].includes(recovery.state))
|
|
@@ -209,8 +216,6 @@ export async function dispatchTaskAction(store, request) {
|
|
|
209
216
|
await applyTaskReleaseAction(task);
|
|
210
217
|
else if (request.id === TASK_HOLD_ACTION)
|
|
211
218
|
await applyTaskHoldAction(task);
|
|
212
|
-
else if (request.id === TASK_START_ACTION)
|
|
213
|
-
await applyOrchestratorStartAction(task);
|
|
214
219
|
else if (request.id === TASK_QUEUE_ACTION)
|
|
215
220
|
await applyTaskQueueAction(job, task);
|
|
216
221
|
else
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export function deriveTaskTitle(input, fallback = 'Task') {
|
|
2
|
+
const first = String(input ?? '').split(/\r?\n/).map(line => line.trim()).find(Boolean) ?? fallback;
|
|
3
|
+
const cleaned = first.replace(/^#{1,6}\s+/, '').replace(/^[-*+]\s+/, '').replace(/^\d+[.)]\s+/, '').replace(/\s+/g, ' ').trim() || fallback;
|
|
4
|
+
return cleaned.length <= 96 ? cleaned : cleaned.slice(0, 93).trimEnd() + '...';
|
|
5
|
+
}
|
package/dist/troubleshooter.js
CHANGED
|
@@ -51,10 +51,10 @@ export function pendingTroubleshooterTasks(job) {
|
|
|
51
51
|
const id = troubleshooterTaskId(task);
|
|
52
52
|
let create = false;
|
|
53
53
|
const decision = dispatchTroubleshooter({
|
|
54
|
-
|
|
54
|
+
failedWork: task.state === 'FAILED',
|
|
55
55
|
ordinaryWork: task.kind !== 'planner' && task.agentName !== 'troubleshooter',
|
|
56
56
|
currentRun: belongsToCurrentRun(job, task),
|
|
57
|
-
needsRecovery:
|
|
57
|
+
needsRecovery: true,
|
|
58
58
|
idUnused: !existing.has(id),
|
|
59
59
|
noRecoveryBarrier: !hasRecoveryBarrier(job, task),
|
|
60
60
|
}, () => { create = true; });
|
package/dist/worker-routing.js
CHANGED
|
@@ -48,7 +48,7 @@ export function usageForProvider(snapshot, provider, now = Date.now()) {
|
|
|
48
48
|
return {};
|
|
49
49
|
}
|
|
50
50
|
export function workerCandidates(adapters, registered, snapshot) {
|
|
51
|
-
return adapters.
|
|
51
|
+
return adapters.flatMap(adapter => {
|
|
52
52
|
const workers = registered.filter(worker => worker.runtime.adapter === adapter.name);
|
|
53
53
|
return (workers.length > 0 ? workers : [undefined]).map(worker => {
|
|
54
54
|
const provider = worker?.runtime.provider ?? (adapter.name === 'chatgpt' ? 'chatgpt' : undefined);
|