agents-relay 2.0.168 → 2.0.170
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 +13 -19
- package/dist/adapters.js +3 -9
- package/dist/blockers.js +3 -2
- package/dist/cli-help.js +3 -3
- package/dist/cli-job-create.js +2 -4
- package/dist/cli-support.js +5 -16
- package/dist/cli.js +9 -11
- package/dist/dashboard-html.js +5 -9
- package/dist/dashboard-style.js +0 -1
- package/dist/execution-recovery.js +1 -1
- package/dist/force-restate-validation.js +11 -1
- package/dist/interactive-resume.js +1 -1
- package/dist/job-actions.js +1 -1
- package/dist/job-pause.js +1 -1
- package/dist/legacy-blocked-task-migration.js +1 -1
- package/dist/local-authority-store.js +0 -57
- package/dist/local-git-merge.js +1 -2
- package/dist/local-merge-delivery.js +1 -1
- package/dist/markers.js +5 -6
- package/dist/planned-review-head.js +2 -2
- package/dist/planner-runtime.js +3 -2
- package/dist/planner.js +2 -7
- package/dist/pool.js +7 -36
- package/dist/reconciler-review.js +4 -5
- package/dist/reconciler.js +11 -36
- package/dist/relay-create.js +3 -3
- package/dist/relay-job-snapshot-reads.js +6 -2
- package/dist/relay-store.js +2 -2
- package/dist/relay-workflow-operations.js +2 -2
- package/dist/relayd-control.js +9 -7
- package/dist/relayd-utilities.js +0 -4
- package/dist/relayd.js +7 -24
- package/dist/runtime-status.js +0 -19
- package/dist/single-job-task-control.js +1 -1
- package/dist/store.js +1 -1
- package/dist/task-actions.js +3 -3
- package/dist/task-recovery.js +2 -2
- package/dist/task-transitions.js +61 -0
- package/dist/troubleshooter-fallback.js +1 -1
- package/dist/troubleshooter.js +1 -4
- package/dist/types.js +1 -2
- package/dist/unblock.js +5 -9
- package/dist/worker-rejection.js +1 -1
- package/dist/workflow/authoritative-conditions.js +1 -1
- package/dist/workflow/blocker-resolution-v2.js +2 -4
- package/dist/workflow/contracts.js +0 -1
- package/dist/workflow/definitions-v2.js +3 -6
- package/dist/workflow/durable-transition.js +1 -1
- package/dist/workflow/force-restate-validation-v2.js +2 -1
- package/dist/workflow/orchestrator-rescue.js +1 -1
- package/dist/workflow/planner-orchestration-v2.js +2 -5
- package/dist/workflow/planner-trigger-v2.js +1 -2
- package/dist/workflow/review-v2.js +0 -1
- package/dist/workflow/task-reconcile.js +1 -7
- package/dist/workflow-effect-failures.js +1 -1
- package/dist/workspace.js +1 -1
- package/package.json +3 -4
- package/skills/agents-relay/SKILL.md +6 -6
- package/skills/agents-relay/agents/planner.agent.md +3 -3
- package/skills/agents-relay/contracts/orchestrator-workflows.md +2 -2
- package/skills/agents-relay/contracts/task-job-contract.md +5 -5
- package/workflow/blocker-resolution.mmd +1 -7
- package/workflow/force-restate-validation.mmd +7 -2
- package/workflow/planner-orchestration.mmd +2 -5
- package/workflow/planner-trigger.mmd +2 -9
- package/workflow/review-lifecycle.mmd +2 -8
- package/dist/e2e-injection.js +0 -76
- package/dist/scheduled-jobs.js +0 -67
- package/dist/scheduler.js +0 -214
- package/dist/workflow/schedule-cycle-v2.js +0 -25
- package/dist/workflow/scheduled-run-v2.js +0 -30
- package/workflow/schedule-cycle.mmd +0 -23
- package/workflow/scheduled-run.mmd +0 -59
|
@@ -3,8 +3,6 @@ import { registerBlockerDependency, resolveBlockerDependencies } from './blocker
|
|
|
3
3
|
import { samePlanDefinition, sameTaskDefinition, DurableDefinitionConflictError, planDefinitionDivergence, taskDefinitionDivergence } from './store-task-definition.js';
|
|
4
4
|
import { mergeInboundJobSnapshot } from './relay-store.js';
|
|
5
5
|
import { publishLocalCommit } from './local-commit-events.js';
|
|
6
|
-
import { decideScheduledRun } from './workflow/scheduled-run-v2.js';
|
|
7
|
-
import { nextScheduleRun, scheduleBucket } from './scheduler.js';
|
|
8
6
|
/**
|
|
9
7
|
* V2 authoritative lifecycle boundary.
|
|
10
8
|
*
|
|
@@ -183,61 +181,6 @@ export class LocalAuthorityDurableStore {
|
|
|
183
181
|
Object.assign(job, structuredClone(updated));
|
|
184
182
|
publishLocalCommit(updated);
|
|
185
183
|
}
|
|
186
|
-
/** Claim a schedule slot under one SQLite IMMEDIATE transaction, across all processes. */
|
|
187
|
-
async claimScheduledSlot(jobId, bucket, now) {
|
|
188
|
-
if (!this.authority.mutateJob)
|
|
189
|
-
throw new Error('Atomic SQLite schedule claim is required');
|
|
190
|
-
let status = 'skipped-idempotent';
|
|
191
|
-
const result = await this.authority.mutateJob(jobId, current => {
|
|
192
|
-
const schedule = current.schedule;
|
|
193
|
-
if (!schedule)
|
|
194
|
-
throw new Error('Scheduled Job ' + jobId + ' has no schedule metadata');
|
|
195
|
-
const slot = new Date(schedule.nextRunAt);
|
|
196
|
-
const decision = decideScheduledRun({
|
|
197
|
-
slotDue: scheduleBucket(slot) === bucket && slot.getTime() <= now.getTime(),
|
|
198
|
-
slotUnclaimed: !schedule.lastScheduledSlot || new Date(schedule.lastScheduledSlot).getTime() < slot.getTime(),
|
|
199
|
-
jobActive: current.state === 'ACTIVE',
|
|
200
|
-
hasActiveTasks: current.tasks.some(task => ['QUEUED', 'RUNNING'].includes(task.state)),
|
|
201
|
-
concurrencySkip: schedule.concurrencyPolicy === 'skip',
|
|
202
|
-
concurrencyQueue: schedule.concurrencyPolicy === 'queue',
|
|
203
|
-
runMissed: schedule.missedRunPolicy === 'run',
|
|
204
|
-
});
|
|
205
|
-
const action = decision.accepted ? decision.actions[0]?.id : undefined;
|
|
206
|
-
if (action === 'schedule.skipActiveAdvance') {
|
|
207
|
-
schedule.lastScheduledSlot = slot.toISOString();
|
|
208
|
-
schedule.nextRunAt = nextScheduleRun(schedule, now).toISOString();
|
|
209
|
-
status = 'skipped-active';
|
|
210
|
-
}
|
|
211
|
-
else if (action === 'schedule.claimFromSlot' || action === 'schedule.claimFromNow') {
|
|
212
|
-
schedule.lastScheduledSlot = slot.toISOString();
|
|
213
|
-
schedule.currentRunSlot = slot.toISOString();
|
|
214
|
-
schedule.nextRunAt = nextScheduleRun(schedule, action === 'schedule.claimFromSlot' ? slot : now).toISOString();
|
|
215
|
-
status = 'completed';
|
|
216
|
-
}
|
|
217
|
-
if (status !== 'skipped-idempotent')
|
|
218
|
-
current.updatedAt = now.toISOString();
|
|
219
|
-
return current;
|
|
220
|
-
});
|
|
221
|
-
if (status !== 'skipped-idempotent')
|
|
222
|
-
publishLocalCommit(result);
|
|
223
|
-
return { job: result, status };
|
|
224
|
-
}
|
|
225
|
-
/** Persist completion only if this is still the authoritative claimed slot. */
|
|
226
|
-
async finishScheduledSlot(jobId, slot, now) {
|
|
227
|
-
if (!this.authority.mutateJob)
|
|
228
|
-
throw new Error('Atomic SQLite schedule completion is required');
|
|
229
|
-
const result = await this.authority.mutateJob(jobId, current => {
|
|
230
|
-
if (current.schedule?.currentRunSlot !== slot)
|
|
231
|
-
return current;
|
|
232
|
-
if (!current.schedule.lastRunAt || Date.parse(current.schedule.lastRunAt) < now.getTime()) {
|
|
233
|
-
current.schedule.lastRunAt = now.toISOString();
|
|
234
|
-
current.updatedAt = now.toISOString();
|
|
235
|
-
}
|
|
236
|
-
return current;
|
|
237
|
-
});
|
|
238
|
-
publishLocalCommit(result);
|
|
239
|
-
return result;
|
|
240
|
-
}
|
|
241
184
|
async appendPlan(plan) {
|
|
242
185
|
await this.mutate(plan.jobId, job => {
|
|
243
186
|
const existing = (job.plans ?? []).find(item => item.id === plan.id);
|
package/dist/local-git-merge.js
CHANGED
|
@@ -3,7 +3,7 @@ import { withLocalMergeLock } from './local-merge-lock.js';
|
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { existsSync, readFileSync } from 'node:fs';
|
|
5
5
|
import { implementationComplete } from './runtime-status.js';
|
|
6
|
-
import { completionGateSatisfied } from './
|
|
6
|
+
import { completionGateSatisfied } from './task-transitions.js';
|
|
7
7
|
function git(cwd, ...args) {
|
|
8
8
|
return execFileSync('git', ['-C', cwd, ...args], {
|
|
9
9
|
encoding: 'utf8', timeout: 30000, maxBuffer: 1024 * 1024,
|
|
@@ -28,7 +28,6 @@ export function localMergeFacts(job, headSha, now = Date.now) {
|
|
|
28
28
|
return {
|
|
29
29
|
jobActive: job.state === 'ACTIVE',
|
|
30
30
|
autoMergeEnabled: job.autoMerge !== false,
|
|
31
|
-
schedulePresent: Boolean(job.schedule),
|
|
32
31
|
implementationComplete: implementationComplete(job),
|
|
33
32
|
completionGateSatisfied: completionGateSatisfied(job),
|
|
34
33
|
reviewSatisfied: job.reviewEnabled === false || job.review?.outcome === 'approved',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { eventFor } from './events.js';
|
|
3
|
-
import { transitionJobEvent } from './
|
|
3
|
+
import { transitionJobEvent } from './task-transitions.js';
|
|
4
4
|
import { deliverLocalMerge, localMergeFacts } from './local-git-merge.js';
|
|
5
5
|
import { errorMessage } from './observability.js';
|
|
6
6
|
import { decideJobLifecycle } from './workflow/lifecycle.js';
|
package/dist/markers.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { currentJobMetadata } from './relay-job-snapshot-reads.js';
|
|
1
2
|
export const JOB_MARKER = 'agents-relay:job:v1';
|
|
2
3
|
export const TASK_MARKER = 'agents-relay:task:v1';
|
|
3
4
|
export const PLAN_MARKER = 'agents-relay:plan:v1';
|
|
@@ -35,7 +36,7 @@ function contentBeforeDelimiter(body, heading, end) {
|
|
|
35
36
|
}
|
|
36
37
|
function jobMetadata(job) {
|
|
37
38
|
const { objective: _objective, description: _description, tasks: _tasks, plans: _plans, ...metadata } = job;
|
|
38
|
-
return metadata;
|
|
39
|
+
return currentJobMetadata(metadata);
|
|
39
40
|
}
|
|
40
41
|
export function renderJobBody(job) {
|
|
41
42
|
const objective = job.objective?.trim() || '_No objective recorded._';
|
|
@@ -85,11 +86,9 @@ export function parseJob(body) {
|
|
|
85
86
|
const bodyObjective = markerEnd >= 0 ? body.slice(markerEnd + 3).trim() || undefined : undefined;
|
|
86
87
|
const objective = job.objective ?? job.description ?? contentBeforeDelimiter(body, '\n## Objective\n\n', JOB_OBJECTIVE_END) ?? markdownSection(body, '\n## Objective\n') ?? bodyObjective;
|
|
87
88
|
const parsed = { ...job, objective };
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return null;
|
|
92
|
-
return parsed;
|
|
89
|
+
// Historical markers may carry mode/schedule; neither is live Job state.
|
|
90
|
+
const { executionMode: _legacyMode, schedule: _legacySchedule, ...current } = parsed;
|
|
91
|
+
return current;
|
|
93
92
|
}
|
|
94
93
|
export function parseTask(body) {
|
|
95
94
|
const task = parseMarker(body, TASK_MARKER);
|
|
@@ -7,8 +7,8 @@ export async function bindPlannedReviewHead(store, task, localHeadSha) {
|
|
|
7
7
|
'Review the current immutable local source head as a PRE-MERGE approval gate.',
|
|
8
8
|
'Distinguish code added by this implementation from pre-existing local baseline/seed files present before the Task started.',
|
|
9
9
|
'Preserved baseline files are not automatically extra changes merely because they differ from main; still review them for real risks.',
|
|
10
|
-
'Do not require a remote PR MERGED, Job COMPLETED, or post-merge
|
|
11
|
-
'those events must follow approval and
|
|
10
|
+
'Do not require a remote PR MERGED, Job COMPLETED, or post-merge validation before review approval:',
|
|
11
|
+
'those events must follow approval and require their own verified durable receipts.',
|
|
12
12
|
].join(' ');
|
|
13
13
|
if (!task.input.includes(rule))
|
|
14
14
|
task.input += '\n\n' + rule;
|
package/dist/planner-runtime.js
CHANGED
|
@@ -8,8 +8,8 @@ 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 './
|
|
12
|
-
import { applyPlannerHistoryCleanup, forceRestateConverged } from './force-restate-validation.js';
|
|
11
|
+
import { tryTransitionTaskEvent } from './task-transitions.js';
|
|
12
|
+
import { applyPlannerHistoryCleanup, forceRestateConverged, objectiveSatisfiedForReview } from './force-restate-validation.js';
|
|
13
13
|
import { decideForceRestateValidation } from './workflow/force-restate-validation-v2.js';
|
|
14
14
|
import { workerCandidates } from './worker-routing.js';
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
@@ -268,6 +268,7 @@ export async function finishPlanner(ports, jobId, planId, executionId, error, ra
|
|
|
268
268
|
historyConsistencyValid: history.valid,
|
|
269
269
|
normalHistoryMutationAbsent: !candidatePlan.forceRestate && historyCleanup.length === 0,
|
|
270
270
|
forwardConvergenceValid: forwardValid,
|
|
271
|
+
objectiveSatisfied: !plan.forceRestate && objectiveSatisfiedForReview(candidate),
|
|
271
272
|
});
|
|
272
273
|
if (!decision.accepted) {
|
|
273
274
|
throw new PlannerApplyValidationError('forward_convergence', [`Plan Apply graph rejected the candidate: ${decision.reason}.`]);
|
package/dist/planner.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { buildReferenceFirstPrompt, renderAgentPrompt } from './agent-prompt-builder.js';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { parseTestInjection } from './e2e-injection.js';
|
|
4
3
|
import { fileURLToPath } from 'node:url';
|
|
5
4
|
import { dirname, join, resolve as resolvePath } from 'node:path';
|
|
6
5
|
import { storySizeTimeoutMs, taskOverallTimeoutMs } from './types.js';
|
|
@@ -31,7 +30,6 @@ function plannerTrigger(job, manual = false) {
|
|
|
31
30
|
const decision = dispatchPlannerTrigger({
|
|
32
31
|
manualPlan: manual && !hasUnblock,
|
|
33
32
|
hasUnblock,
|
|
34
|
-
scheduledRun: Boolean(job.schedule?.currentRunSlot),
|
|
35
33
|
initialPlan: (job.plans?.length ?? 0) === 0 && !job.tasks.some(task => task.kind === 'planner'),
|
|
36
34
|
});
|
|
37
35
|
if (!decision.accepted)
|
|
@@ -57,8 +55,6 @@ function plannerTrigger(job, manual = false) {
|
|
|
57
55
|
throw new Error('Planner trigger graph selected unblock without unblock facts');
|
|
58
56
|
return { source: 'unblock', taskIds: cycle.taskIds, reason: cycle.reason, facts: cycle.facts, blockedHeadShas: cycle.baselineHeadShas, currentHeadSha: cycle.currentHeadSha, unblockSource: cycle.source };
|
|
59
57
|
}
|
|
60
|
-
if (source === 'schedule')
|
|
61
|
-
return { source: 'schedule' };
|
|
62
58
|
if (source === 'initial')
|
|
63
59
|
return { source: 'initial' };
|
|
64
60
|
return { source: 'continuation' };
|
|
@@ -118,14 +114,14 @@ export function materializePlannerChild(job, planner, spec, executionContext) {
|
|
|
118
114
|
throw new Error(`Planner resolution for ${spec.id} must target one unresolved durable Task`);
|
|
119
115
|
}
|
|
120
116
|
const reviewerThread = spec.agentName === 'reviewer' ? prepareReviewerThreadDelivery(job, { id: spec.id, agentName: spec.agentName, reviewHeadSha }) : undefined, implementationThread = (spec.agentName === 'implementation' || spec.agentName == null) && job.implementationThreadId ? job.implementationThreadId : undefined, threadOwned = Boolean(reviewerThread || implementationThread);
|
|
121
|
-
return createTaskEntity(job, { jobId: job.id, id: spec.id, title: spec.agentName === 'reviewer' ? 'Review' : 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: objectiveRoot?.id ?? spec.parentTaskId ?? null, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], workerRequirements: threadOwned ? chatgptThreadRequirements(spec.workerRequirements) : spec.workerRequirements,
|
|
117
|
+
return createTaskEntity(job, { jobId: job.id, id: spec.id, title: spec.agentName === 'reviewer' ? 'Review' : 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: objectiveRoot?.id ?? spec.parentTaskId ?? null, dependencies: spec.dependencies ?? [], capabilities: spec.capabilities ?? [], workerRequirements: threadOwned ? chatgptThreadRequirements(spec.workerRequirements) : spec.workerRequirements, adapter: threadOwned ? 'chatgpt' : (spec.adapter ?? '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 });
|
|
122
118
|
}
|
|
123
119
|
export function planningSpec(task) {
|
|
124
120
|
if (!task.output)
|
|
125
121
|
throw new Error(`Initial task ${task.id} requires an output contract`);
|
|
126
122
|
if (!['codex', 'chatgpt'].includes(task.adapter))
|
|
127
123
|
throw new Error(`Initial task ${task.id} requires a supported worker adapter`);
|
|
128
|
-
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,
|
|
124
|
+
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, 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 };
|
|
129
125
|
}
|
|
130
126
|
function record(value, field = 'result') {
|
|
131
127
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -318,7 +314,6 @@ export function parsePlannerResult(value) {
|
|
|
318
314
|
id, title: requiredPlannerTaskTitle(task.title, index), parentTaskId, input: task.input, priority, storySize,
|
|
319
315
|
projectName: optionalString(task.projectName, 'projectName'), agentName: optionalString(task.agentName, 'agentName'), agentGraph, review,
|
|
320
316
|
dependencies: stringArray(task.dependencies, 'dependencies'), capabilities: stringArray(task.capabilities, 'capabilities'), workerRequirements: requirements,
|
|
321
|
-
testInjection: parseTestInjection(task.testInjection ?? task['--test-injection']) ?? optionalString(task.testInjection ?? task['--test-injection'], `next_tasks[${index}].--test-injection`),
|
|
322
317
|
adapter: adapter, output: output, routing: route,
|
|
323
318
|
continuation: task.continuation, maxAttempts, timeoutMs, resolution: resolutionProposal(task.resolution, `next_tasks[${index}].resolution`)
|
|
324
319
|
};
|
package/dist/pool.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { listManagedGitHubJobs } from './store.js';
|
|
2
2
|
import { eventFor } from './events.js';
|
|
3
|
-
import { runScheduledBucketFromStore } from './scheduled-jobs.js';
|
|
4
|
-
import { scheduleBucket } from './scheduler.js';
|
|
5
3
|
import { ReconcileCoordinator } from './reconcile-coordinator.js';
|
|
6
4
|
import { errorMessage, observability, withObservabilityCorrelation } from './observability.js';
|
|
7
5
|
import { jobHasActiveExecution } from './job-execution-slot.js';
|
|
8
|
-
import {
|
|
6
|
+
import { pendingPlan } from './planner.js';
|
|
7
|
+
import { decidePlannerOrchestration, plannerOrchestrationFacts } from './workflow/planner-orchestration-v2.js';
|
|
9
8
|
import { pendingTroubleshooterRetries } from './troubleshooter.js';
|
|
10
9
|
const RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
11
10
|
export function compareJobPriority(a, b) { return RANK[a.priority ?? 'P2'] - RANK[b.priority ?? 'P2'] || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id); }
|
|
@@ -19,7 +18,9 @@ export function needsLifecycleReconciliation(job, now = Date.now()) {
|
|
|
19
18
|
if (job.state === 'ACTIVE' && job.localMerge?.status === 'failed' &&
|
|
20
19
|
(!job.localMerge.nextAttemptAt || Date.parse(job.localMerge.nextAttemptAt) <= now))
|
|
21
20
|
return true;
|
|
22
|
-
|
|
21
|
+
// The Graph owns continuation eligibility, including restart recovery.
|
|
22
|
+
const planning = decidePlannerOrchestration(plannerOrchestrationFacts(job, Boolean(pendingPlan(job))));
|
|
23
|
+
if (planning.accepted && planning.actions.length > 0)
|
|
23
24
|
return true;
|
|
24
25
|
if (pendingTroubleshooterRetries(job).length > 0)
|
|
25
26
|
return true;
|
|
@@ -44,7 +45,6 @@ export class RepositoryWorkerPool {
|
|
|
44
45
|
pending = false;
|
|
45
46
|
reconcileLocks = new Map();
|
|
46
47
|
coordinator;
|
|
47
|
-
scheduleSignalCoordinator;
|
|
48
48
|
dispatchTail = Promise.resolve();
|
|
49
49
|
watchedBus;
|
|
50
50
|
constructor(concurrency, makeReconciler, localInventory, resolveLocalJob) {
|
|
@@ -61,7 +61,6 @@ export class RepositoryWorkerPool {
|
|
|
61
61
|
});
|
|
62
62
|
};
|
|
63
63
|
this.coordinator = new ReconcileCoordinator((jobId, signals) => this.consumeSignals(jobId, signals), reportCoordinatorError);
|
|
64
|
-
this.scheduleSignalCoordinator = new ReconcileCoordinator((jobId, signals) => this.consumeScheduleSignals(jobId, signals), reportCoordinatorError);
|
|
65
64
|
}
|
|
66
65
|
async localTarget(jobId) {
|
|
67
66
|
return this.resolveLocalJob(jobId);
|
|
@@ -107,20 +106,6 @@ export class RepositoryWorkerPool {
|
|
|
107
106
|
}
|
|
108
107
|
return `event.${event.type}`;
|
|
109
108
|
}
|
|
110
|
-
async consumeScheduleSignals(jobId, signals) {
|
|
111
|
-
const event = [...signals].reverse().find(item => item.event?.type === 'schedule.tick')?.event;
|
|
112
|
-
const bucket = typeof event?.data?.bucket === 'string' ? event.data.bucket : null;
|
|
113
|
-
const tickAt = new Date(typeof event?.data?.tickAt === 'string' ? event.data.tickAt : event?.timestamp ?? Date.now());
|
|
114
|
-
if (!bucket || Number.isNaN(tickAt.getTime()))
|
|
115
|
-
return;
|
|
116
|
-
const target = await this.localTarget(jobId);
|
|
117
|
-
if (!target)
|
|
118
|
-
return;
|
|
119
|
-
await runScheduledBucketFromStore(target.store, { jobId, bucket }, {
|
|
120
|
-
reconcile: async () => this.reconcileLocalJob(jobId, 'schedule.tick'),
|
|
121
|
-
idle: async () => { },
|
|
122
|
-
}, tickAt);
|
|
123
|
-
}
|
|
124
109
|
async reconcileLocalJob(jobId, reason = 'local.request') {
|
|
125
110
|
const target = await this.localTarget(jobId);
|
|
126
111
|
if (!target)
|
|
@@ -164,7 +149,8 @@ export class RepositoryWorkerPool {
|
|
|
164
149
|
const expiredRunning = job.tasks.some(task => ['RUNNING', 'QUEUED'].includes(task.state) && task.executionId !== null && task.leaseExpiresAt !== null && new Date(task.leaseExpiresAt).getTime() <= Date.now());
|
|
165
150
|
if (!needsLifecycleReconciliation(job))
|
|
166
151
|
continue;
|
|
167
|
-
|
|
152
|
+
// A Graph-selected Planner continuation does not reserve a Task worker slot.
|
|
153
|
+
const allowance = ready || expiredRunning ? Math.max(1, Math.min(capacity, ready || 1)) : 0;
|
|
168
154
|
try {
|
|
169
155
|
await this.reconcileExclusive(job.id, async () => {
|
|
170
156
|
await this.makeReconciler(store, allowance).reconcile(job.id);
|
|
@@ -227,10 +213,6 @@ export class RepositoryWorkerPool {
|
|
|
227
213
|
const unsubscribe = await bus.subscribeAll(async (event) => {
|
|
228
214
|
const routedWebhook = event.type === 'github.webhook' && event.data?.coordinatorRouted === true;
|
|
229
215
|
const ingressWebhook = event.type === 'github.webhook' && event.data?.coordinatorIngress === true && !routedWebhook;
|
|
230
|
-
if (event.type === 'schedule.tick') {
|
|
231
|
-
await this.scheduleSignalCoordinator.acceptTransport({ jobId: event.job_id, reason: this.signalReason(event), event });
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
216
|
if (ingressWebhook)
|
|
235
217
|
return;
|
|
236
218
|
if (routedWebhook)
|
|
@@ -254,17 +236,6 @@ export class RepositoryWorkerPool {
|
|
|
254
236
|
this.watchedBus = undefined;
|
|
255
237
|
};
|
|
256
238
|
}
|
|
257
|
-
async scheduleTickSignal(bus, job, now = new Date()) {
|
|
258
|
-
if (!job.schedule)
|
|
259
|
-
return;
|
|
260
|
-
const event = eventFor(job.id, 'scheduler', null, 'schedule.tick', 'queued', `Schedule tick for ${job.schedule.nextRunAt}`, 'orchestrator', { bucket: scheduleBucket(new Date(job.schedule.nextRunAt)), tickAt: now.toISOString() }, { agent: 'relay', component: 'scheduler-timer' });
|
|
261
|
-
// Use the same coordinator-owned schedule path whether transport is available or not.
|
|
262
|
-
if (bus) {
|
|
263
|
-
await bus.publish(event);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
await this.scheduleSignalCoordinator.accept({ jobId: job.id, reason: this.signalReason(event), event });
|
|
267
|
-
}
|
|
268
239
|
async webhookSignal(_bus, _wake) {
|
|
269
240
|
// Uncorrelated webhook wakeups are intentionally ignored. A caller must first
|
|
270
241
|
// resolve the target from local cached state and then call webhookWake().
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { storySizeTimeoutMs, taskOverallTimeoutMs } from './types.js';
|
|
2
|
-
import { completionGateSatisfied,
|
|
2
|
+
import { completionGateSatisfied, refreshTaskDependencies, transitionTaskEvent, tryTransitionTaskEvent } from './task-transitions.js';
|
|
3
3
|
import { implementationComplete, implementationTasks } from './runtime-status.js';
|
|
4
4
|
import { eventFor } from './events.js';
|
|
5
5
|
import { workerCandidates } from './worker-routing.js';
|
|
@@ -37,7 +37,6 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
37
37
|
const reviewTasks = job.tasks.filter(task => task.agentName === 'reviewer' && task.reviewHeadSha === headSha);
|
|
38
38
|
const currentReviewTask = job.review?.taskId ? job.tasks.find(task => task.id === job.review?.taskId) : undefined;
|
|
39
39
|
return {
|
|
40
|
-
unscheduled: !job.schedule,
|
|
41
40
|
reviewEnabled: job.reviewEnabled !== false,
|
|
42
41
|
jobActive: job.state === 'ACTIVE' || job.projectionRecovery?.status === 'repairing',
|
|
43
42
|
implementationComplete: implementationComplete(job),
|
|
@@ -149,7 +148,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
149
148
|
'Review the complete PR diff at this exact immutable head SHA, not only the dependency Tasks.',
|
|
150
149
|
'Use the selected reviewer agent contract as the authoritative review protocol.',
|
|
151
150
|
'This is a PRE-MERGE artifact approval gate: inspect exact PR head, requirements, tests and genuine blocking defects.',
|
|
152
|
-
'Do not block approval because automatic merge, Job COMPLETED, or post-merge
|
|
151
|
+
'Do not block approval because automatic merge, Job COMPLETED, or post-merge validation has not occurred yet.',
|
|
153
152
|
'Do not treat historical failed attempts as current artifact defects; distinguish preserved baseline files from implementation.',
|
|
154
153
|
].join('\n\n');
|
|
155
154
|
const task = createTaskEntity(job, {
|
|
@@ -213,7 +212,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
213
212
|
job.review = { outcome, headSha, taskId: reviewTaskId, evidence: null, updatedAt: now };
|
|
214
213
|
job.updatedAt = now;
|
|
215
214
|
await this.store.saveJob(job);
|
|
216
|
-
const ready =
|
|
215
|
+
const ready = refreshTaskDependencies(await this.store.load(job.id));
|
|
217
216
|
await this.store.saveJob(ready);
|
|
218
217
|
}
|
|
219
218
|
async selectReviewer(implementationTasks, requirementsOverride) {
|
|
@@ -290,7 +289,7 @@ export class ReconcilerReviewBase extends ReconcilerRuntimeBase {
|
|
|
290
289
|
job: {
|
|
291
290
|
id: job.id, title: job.title, objective: job.objective ?? null,
|
|
292
291
|
description: job.description ?? null, priority: job.priority ?? null,
|
|
293
|
-
|
|
292
|
+
repository: job.repository,
|
|
294
293
|
prNumber: this.externalReviewPrNumber(job), state: job.state,
|
|
295
294
|
},
|
|
296
295
|
task: {
|
package/dist/reconciler.js
CHANGED
|
@@ -5,12 +5,11 @@ import { drainProductionWorkflowOutbox } from './reconciler-workflow-outbox.js';
|
|
|
5
5
|
import { cancelPausedGenerations } from './reconciler-pause.js';
|
|
6
6
|
import { taskCommentContext } from './task-comments.js';
|
|
7
7
|
import { cancelActiveTasks } from './reconciler-cancellation.js';
|
|
8
|
-
import { authorizedInjection } from './e2e-injection.js';
|
|
9
8
|
import { implementationComplete } from './runtime-status.js';
|
|
10
9
|
import { bindPlannedReviewHead } from './planned-review-head.js';
|
|
11
10
|
import { randomUUID } from 'node:crypto';
|
|
12
11
|
import { storySizeTimeoutMs, chatGptStartAckTimeoutMs } from './types.js';
|
|
13
|
-
import { dependencyReady,
|
|
12
|
+
import { dependencyReady, refreshTaskDependencies, transitionTaskEvent } from './task-transitions.js';
|
|
14
13
|
import { applyTaskRetryAction } from './task-actions.js';
|
|
15
14
|
import { LaunchFailure, StartAckTimeout } from './adapters.js';
|
|
16
15
|
import { eventFor } from './events.js';
|
|
@@ -57,7 +56,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
57
56
|
this.workflowEffects.register(new CallbackWorkflowEffectHandler('effect.cleanupJobWorktree', async (context) => cleanupMergedJobWorktree(await this.store.load(context.jobId), this.options.projectPathForJob), context => `effect.cleanupJobWorktree:${context.jobId}`));
|
|
58
57
|
}
|
|
59
58
|
async dispatchExecutableTasks(jobId) {
|
|
60
|
-
const scheduled =
|
|
59
|
+
const scheduled = refreshTaskDependencies(await this.store.load(jobId));
|
|
61
60
|
if (scheduled.pause)
|
|
62
61
|
return scheduled;
|
|
63
62
|
for (const task of scheduled.tasks.filter(item => item.state === 'SUCCEEDED' && (item.continuation || scheduled.continuation) && !item.continuationDeliveredAt)) {
|
|
@@ -78,7 +77,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
78
77
|
await this.applyTaskEffectOutcomes(scheduled, task, outcomes);
|
|
79
78
|
}, async () => { await this.store.saveTask(task); });
|
|
80
79
|
}
|
|
81
|
-
return
|
|
80
|
+
return refreshTaskDependencies(await this.store.load(jobId));
|
|
82
81
|
}
|
|
83
82
|
async reconcile(jobId) {
|
|
84
83
|
if (this.reconciling)
|
|
@@ -174,8 +173,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
174
173
|
}
|
|
175
174
|
const beforeSchedule = await this.store.load(jobId);
|
|
176
175
|
const taskStatesBeforeSchedule = new Map(beforeSchedule.tasks.map(task => [task.id, task.state]));
|
|
177
|
-
|
|
178
|
-
let scheduled = schedule(beforeSchedule);
|
|
176
|
+
let scheduled = refreshTaskDependencies(beforeSchedule);
|
|
179
177
|
for (const task of scheduled.tasks)
|
|
180
178
|
if (task.executionId && ['QUEUED', 'RUNNING'].includes(task.state))
|
|
181
179
|
this.armInactivityWatchdog(task, task.executionId);
|
|
@@ -183,19 +181,17 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
183
181
|
if (taskStatesBeforeSchedule.get(task.id) !== task.state)
|
|
184
182
|
await this.store.saveTask(task);
|
|
185
183
|
}
|
|
186
|
-
if (scheduled.state !== previousState
|
|
184
|
+
if (scheduled.state !== previousState)
|
|
187
185
|
await this.store.saveJob(scheduled);
|
|
188
186
|
{
|
|
189
187
|
await this.ensureFinalReview(scheduled, headSha);
|
|
190
|
-
scheduled =
|
|
191
|
-
}
|
|
192
|
-
if (scheduled.executionMode === 'autonomous' || (scheduled.plans ?? []).some(plan => plan.forceRestate && plan.status === 'FAILED')) {
|
|
193
|
-
scheduled = schedule(await this.maybeCreatePlanner(scheduled));
|
|
188
|
+
scheduled = refreshTaskDependencies(await this.store.load(jobId));
|
|
194
189
|
}
|
|
190
|
+
scheduled = refreshTaskDependencies(await this.maybeCreatePlanner(scheduled));
|
|
195
191
|
if (scheduled.state !== previousState || scheduled.updatedAt !== job.updatedAt)
|
|
196
192
|
await this.store.saveJob(scheduled);
|
|
197
193
|
scheduled = await this.dispatchExecutableTasks(jobId);
|
|
198
|
-
let finalState =
|
|
194
|
+
let finalState = refreshTaskDependencies(await this.store.load(jobId));
|
|
199
195
|
if (finalState.updatedAt !== scheduled.updatedAt)
|
|
200
196
|
await this.store.saveJob(finalState);
|
|
201
197
|
await this.ensureFinalReview(finalState, headSha);
|
|
@@ -277,6 +273,9 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
277
273
|
return executeManualPlan(this.store, jobId, guidance, this.options.plannerTimeoutMs ?? storySizeTimeoutMs('M'), (job, plan) => this.launchPlanner(job, plan, true));
|
|
278
274
|
}
|
|
279
275
|
async maybeCreatePlanner(job) {
|
|
276
|
+
// Terminal Jobs can repair projections and re-review, but must not reopen planning.
|
|
277
|
+
if (job.state !== 'ACTIVE')
|
|
278
|
+
return job;
|
|
280
279
|
const pending = pendingPlan(job);
|
|
281
280
|
const recoverable = pending && !this.plannerLive.has(pending.id)
|
|
282
281
|
&& (!pending.executionId || pending.leaseOwner !== this.options.owner || !pending.leaseExpiresAt || Date.parse(pending.leaseExpiresAt) <= (this.options.now ?? new Date()).getTime())
|
|
@@ -470,31 +469,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
470
469
|
const startAckSetup = task.adapter === 'chatgpt' ? await this.startAck(job.id, task.id, sourceId, Date.now(), this.options.startAckTimeoutMs ?? chatGptStartAckTimeoutMs(task.timeoutMs)) : null;
|
|
471
470
|
const closePolicy = task.chatgptTabClosePolicy ?? this.options.chatgptTabClosePolicy ?? adapter.tabClosePolicy ?? 'after-start';
|
|
472
471
|
const terminalWait = task.adapter === 'chatgpt' && closePolicy === 'after-terminal' ? this.terminalWait(task.id, durableExecutionId, startedAt - 1) : null;
|
|
473
|
-
const declaration = authorizedInjection(job.repository, task.testInjection);
|
|
474
|
-
if (task.testInjection && !declaration) {
|
|
475
|
-
await this.failLaunch(job.id, task.id, durableExecutionId, `E2E test injection refused for repository ${job.repository}; configure E2E_REPO on relayd`, 'delivery');
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
const workerCheckpoint = declaration?.method === 'worker.codex.launch' || declaration?.method === 'browserWorkspace.command';
|
|
479
|
-
const testInjection = workerCheckpoint && ((task.adapter === 'codex' && declaration.method === 'worker.codex.launch') || (task.adapter === 'chatgpt' && declaration.method === 'browserWorkspace.command')) ? declaration : undefined;
|
|
480
|
-
if (workerCheckpoint && !testInjection) {
|
|
481
|
-
await this.failLaunch(job.id, task.id, durableExecutionId, `E2E test injection ${declaration.method} is incompatible with adapter ${task.adapter}`, 'delivery');
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
if (testInjection) {
|
|
485
|
-
const durable = (await this.store.load(job.id)).tasks.find(item => item.id === task.id);
|
|
486
|
-
if (!durable || durable.executionId !== durableExecutionId)
|
|
487
|
-
return;
|
|
488
|
-
delete durable.testInjection;
|
|
489
|
-
durable.updatedAt = new Date(Math.max(Date.now(), Date.parse(durable.updatedAt) + 1)).toISOString();
|
|
490
|
-
await this.store.saveTask(durable);
|
|
491
|
-
const consumed = (await this.store.load(job.id)).tasks.find(item => item.id === task.id);
|
|
492
|
-
if (!consumed || consumed.executionId !== durableExecutionId || consumed.testInjection)
|
|
493
|
-
throw new Error(`E2E injection consumption was not durably committed for ${task.id}`);
|
|
494
|
-
delete task.testInjection;
|
|
495
|
-
}
|
|
496
472
|
const launchContext = {
|
|
497
|
-
...(testInjection ? { testInjection } : {}),
|
|
498
473
|
...(startAckSetup ? { waitForWorkerStart: startAckSetup.ack } : {}),
|
|
499
474
|
...(terminalWait ? { waitForTerminal: terminalWait.promise } : {}),
|
|
500
475
|
executionContext,
|
package/dist/relay-create.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { currentJobMetadata, normalizeRelayJob } from './relay-job-snapshot-reads.js';
|
|
2
2
|
export function createJobIfAbsent(db, job) {
|
|
3
3
|
db.exec('BEGIN IMMEDIATE');
|
|
4
4
|
try {
|
|
5
5
|
const row = db.prepare('SELECT json FROM job_snapshot WHERE job_id=?').get(job.id);
|
|
6
6
|
if (row?.json) {
|
|
7
7
|
db.exec('COMMIT');
|
|
8
|
-
return
|
|
8
|
+
return normalizeRelayJob(row.json);
|
|
9
9
|
}
|
|
10
|
-
db.prepare('INSERT INTO job_snapshot(job_id,json,updated_at) VALUES(?,?,?)').run(job.id, JSON.stringify(job), new Date().toISOString());
|
|
10
|
+
db.prepare('INSERT INTO job_snapshot(job_id,json,updated_at) VALUES(?,?,?)').run(job.id, JSON.stringify(currentJobMetadata(job)), new Date().toISOString());
|
|
11
11
|
db.exec('COMMIT');
|
|
12
12
|
return structuredClone(job);
|
|
13
13
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { normalizeLegacyTaskLifecycle } from './types.js';
|
|
2
2
|
import { normalizeLegacyJobPrNumber } from './job-pr-number-migration.js';
|
|
3
|
+
export function currentJobMetadata(job) {
|
|
4
|
+
const { executionMode: _mode, schedule: _schedule, ...current } = job;
|
|
5
|
+
return current;
|
|
6
|
+
}
|
|
3
7
|
export function normalizeRelayJob(json) {
|
|
4
|
-
const job = normalizeLegacyJobPrNumber(JSON.parse(json));
|
|
5
|
-
job.tasks = job.tasks.map(t => normalizeLegacyTaskLifecycle(t
|
|
8
|
+
const job = currentJobMetadata(normalizeLegacyJobPrNumber(JSON.parse(json)));
|
|
9
|
+
job.tasks = job.tasks.map(t => normalizeLegacyTaskLifecycle(t));
|
|
6
10
|
job.plans = (job.plans ?? []).map(plan => {
|
|
7
11
|
const legacy = plan;
|
|
8
12
|
if (plan.forceRestate === undefined && legacy.forceCleanHistory === true)
|
package/dist/relay-store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadRelayJob, listRelayJobs } from './relay-job-snapshot-reads.js';
|
|
1
|
+
import { currentJobMetadata, 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';
|
|
@@ -242,7 +242,7 @@ export class SQLiteRelayStore {
|
|
|
242
242
|
this.db.prepare(`
|
|
243
243
|
INSERT INTO job_snapshot(job_id,json,updated_at) VALUES(?,?,?)
|
|
244
244
|
ON CONFLICT(job_id) DO UPDATE SET json=excluded.json, updated_at=excluded.updated_at
|
|
245
|
-
`).run(job.id, JSON.stringify(job), now);
|
|
245
|
+
`).run(job.id, JSON.stringify(currentJobMetadata(job)), now);
|
|
246
246
|
}
|
|
247
247
|
async loadAgents() {
|
|
248
248
|
const observed = this.scopeJobId
|
|
@@ -52,7 +52,7 @@ export class SQLiteWorkflowOperations {
|
|
|
52
52
|
if (!row?.json)
|
|
53
53
|
throw new Error(`Unknown Job ${jobId}`);
|
|
54
54
|
const current = normalizeRelayJob(row.json);
|
|
55
|
-
current.tasks = current.tasks.map(task => normalizeLegacyTaskLifecycle(task
|
|
55
|
+
current.tasks = current.tasks.map(task => normalizeLegacyTaskLifecycle(task));
|
|
56
56
|
const previous = structuredClone(current);
|
|
57
57
|
const next = mutation(current);
|
|
58
58
|
if (next.id !== jobId)
|
|
@@ -121,7 +121,7 @@ export class SQLiteWorkflowOperations {
|
|
|
121
121
|
if (!row?.json)
|
|
122
122
|
throw new Error(`Local authoritative Job ${jobId} not found`);
|
|
123
123
|
const current = normalizeRelayJob(row.json);
|
|
124
|
-
current.tasks = current.tasks.map(task => normalizeLegacyTaskLifecycle(task
|
|
124
|
+
current.tasks = current.tasks.map(task => normalizeLegacyTaskLifecycle(task));
|
|
125
125
|
const previous = structuredClone(current);
|
|
126
126
|
const before = JSON.stringify(current);
|
|
127
127
|
const next = mutation(current);
|
package/dist/relayd-control.js
CHANGED
|
@@ -8,7 +8,7 @@ import { classifyFailedTask, supersedeCancelledTask } from './task-actions.js';
|
|
|
8
8
|
import { requestTroubleshooterEscalation, resumeTroubleshooter } from './task-recovery.js';
|
|
9
9
|
import { adoptExistingPullRequestJob } from './cli-job-create.js';
|
|
10
10
|
export function createWorkspaceControl(deps) {
|
|
11
|
-
const { client, trusted, noGitHub, repositories, refreshAll, requireLocalJobBinding, makeReconciler, concurrency, pool, bus, isEffectivelyBlocked,
|
|
11
|
+
const { client, trusted, noGitHub, repositories, refreshAll, requireLocalJobBinding, makeReconciler, concurrency, pool, bus, isEffectivelyBlocked, unblockTask, publishTaskUnblocked, dispatchTaskAction, registerBlockerDependency, discoverAgents, ensureManagedGitHubJob } = deps;
|
|
12
12
|
const workspaceControl = async (body) => {
|
|
13
13
|
const command = typeof body.command === 'string' ? body.command : '';
|
|
14
14
|
const jobId = typeof body.jobId === 'string' ? body.jobId : '';
|
|
@@ -19,6 +19,8 @@ export function createWorkspaceControl(deps) {
|
|
|
19
19
|
const options = (body.options && typeof body.options === 'object' ? body.options : {});
|
|
20
20
|
if (!repository)
|
|
21
21
|
throw new Error('job.create requires repository');
|
|
22
|
+
if ('executionMode' in options || 'schedule' in options)
|
|
23
|
+
throw new Error('Job execution modes and schedules are unsupported');
|
|
22
24
|
const result = await ensureManagedGitHubJob(client, repository, trusted, {
|
|
23
25
|
id: typeof options.id === 'string' && options.id ? options.id : undefined,
|
|
24
26
|
idempotencyKey: typeof options.idempotencyKey === 'string' ? options.idempotencyKey : undefined,
|
|
@@ -26,8 +28,6 @@ export function createWorkspaceControl(deps) {
|
|
|
26
28
|
objective: typeof options.objective === 'string' ? options.objective : undefined,
|
|
27
29
|
projectPath: typeof options.projectPath === 'string' ? options.projectPath : undefined,
|
|
28
30
|
priority: options.priority,
|
|
29
|
-
executionMode: options.executionMode,
|
|
30
|
-
schedule: options.schedule,
|
|
31
31
|
tag: options.tag,
|
|
32
32
|
reviewEnabled: typeof options.reviewEnabled === 'boolean' ? options.reviewEnabled : undefined,
|
|
33
33
|
continuation: options.continuation,
|
|
@@ -44,7 +44,7 @@ export function createWorkspaceControl(deps) {
|
|
|
44
44
|
const current = result.job;
|
|
45
45
|
const existingPlan = current.plans?.at(-1);
|
|
46
46
|
if (existingPlan)
|
|
47
|
-
return { repository, pr: result.pr.number, job: current.id,
|
|
47
|
+
return { repository, pr: result.pr.number, job: current.id, state: current.state, initialPlan: existingPlan.id, reused: true };
|
|
48
48
|
const unpublished = new InMemoryStore(current);
|
|
49
49
|
const reconciler = makeReconciler(unpublished, concurrency);
|
|
50
50
|
try {
|
|
@@ -60,13 +60,13 @@ export function createWorkspaceControl(deps) {
|
|
|
60
60
|
unpublishedStore.relay.close();
|
|
61
61
|
}
|
|
62
62
|
await pool.reconcileLocalJob(result.job.id, 'control.job.create.initial-plan');
|
|
63
|
-
return { repository, pr: committed.prNumber, job: committed.id,
|
|
63
|
+
return { repository, pr: committed.prNumber, job: committed.id, state: committed.state, initialPlan: committed.plans?.at(-1)?.id };
|
|
64
64
|
}
|
|
65
65
|
finally {
|
|
66
66
|
await reconciler.close();
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
return { repository, pr: result.pr.number, job: result.job.id,
|
|
69
|
+
return { repository, pr: result.pr.number, job: result.job.id, state: result.job.state };
|
|
70
70
|
}
|
|
71
71
|
if (command === 'job.adopt') {
|
|
72
72
|
const repository = typeof body.repository === 'string' ? body.repository : '';
|
|
@@ -91,7 +91,9 @@ export function createWorkspaceControl(deps) {
|
|
|
91
91
|
const job = await store.load(jobId);
|
|
92
92
|
if (command === 'job.update') {
|
|
93
93
|
const patch = (body.patch && typeof body.patch === 'object' ? body.patch : {});
|
|
94
|
-
|
|
94
|
+
if ('executionMode' in patch || 'schedule' in patch)
|
|
95
|
+
throw new Error('Job execution modes and schedules are unsupported');
|
|
96
|
+
const mutable = ['title', 'objective', 'projectPath', 'priority', 'tag', 'autoMerge', 'reviewEnabled', 'reviewerThreadId', 'creatorThreadId', 'implementationThreadId', 'continuation', 'localMerge'];
|
|
95
97
|
for (const key of mutable)
|
|
96
98
|
if (Object.prototype.hasOwnProperty.call(patch, key)) {
|
|
97
99
|
const value = patch[key];
|
package/dist/relayd-utilities.js
CHANGED
|
@@ -26,10 +26,6 @@ export function mergeOperationalInventory(current, discovered) {
|
|
|
26
26
|
return [...merged.values()];
|
|
27
27
|
}
|
|
28
28
|
const PR_REFRESH_COALESCE_MS = 1000;
|
|
29
|
-
export const SCHEDULE_HEARTBEAT_MS = 600_000;
|
|
30
|
-
export function dueScheduledOverviews(overviews, now = new Date()) {
|
|
31
|
-
return overviews.filter(item => item.job.state === 'ACTIVE' && item.job.executionMode === 'autonomous' && item.job.schedule && Date.parse(item.job.schedule.nextRunAt) <= now.getTime());
|
|
32
|
-
}
|
|
33
29
|
export function shouldCoalescePullRequestRefresh(completedAt, now = Date.now(), force = false) {
|
|
34
30
|
return !force && completedAt !== null && now >= completedAt && now - completedAt < PR_REFRESH_COALESCE_MS;
|
|
35
31
|
}
|