agents-relay 2.0.107 → 2.0.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-help.js +10 -2
- package/dist/cli.js +15 -1
- package/dist/job-pause.js +75 -0
- package/dist/manual-plan.js +4 -0
- package/dist/planner-runtime.js +2 -0
- package/dist/pool.js +2 -0
- package/dist/reconciler-pause.js +11 -0
- package/dist/reconciler.js +12 -1
- package/dist/scheduler.js +2 -0
- package/dist/workflow/contracts.js +1 -0
- package/package.json +1 -1
- package/workflow/task-lifecycle.mmd +1 -0
package/dist/cli-help.js
CHANGED
|
@@ -8,7 +8,7 @@ Usage:
|
|
|
8
8
|
--no-github: SQLite/Graph-only mode. GitHub auth/API, PR creation, merge and fetch are disabled. Local completion is Graph-owned.
|
|
9
9
|
|
|
10
10
|
Commands:
|
|
11
|
-
job create|update|show|list|merge|unblock|cancel Manage SQLite-authoritative jobs with optional GitHub PRs
|
|
11
|
+
job create|update|show|list|merge|unblock|cancel|pause|resume Manage SQLite-authoritative jobs with optional GitHub PRs
|
|
12
12
|
task create|update|show|list|complete|retry|unblock|cancel Manage durable tasks
|
|
13
13
|
review request Request graph-owned review (optional thread override)
|
|
14
14
|
plan Invoke the Planner for an existing managed Job
|
|
@@ -29,7 +29,7 @@ Use 'node dist/cli.js' only for explicit local development/demo mode.`,
|
|
|
29
29
|
job: `Manage SQLite-authoritative durable jobs.
|
|
30
30
|
|
|
31
31
|
Usage:
|
|
32
|
-
npx agents-relay job <create|update|show|list|merge|unblock|cancel> [options]
|
|
32
|
+
npx agents-relay job <create|update|show|list|merge|unblock|cancel|pause|resume> [options]
|
|
33
33
|
|
|
34
34
|
Actions:
|
|
35
35
|
create Create a durable Job, optionally binding an existing unmanaged PR
|
|
@@ -39,6 +39,8 @@ Actions:
|
|
|
39
39
|
merge Queue a retryable external PR merge; Graph completes the Job only after confirmation
|
|
40
40
|
unblock Authoritatively invalidate current BLOCKED decisions and trigger replanning/resume
|
|
41
41
|
cancel Close the backing PR and cancel the Job
|
|
42
|
+
pause Persistently stop dispatch and interrupt active generations; lifecycle state remains unchanged
|
|
43
|
+
resume Clear pause metadata and requeue interrupted work
|
|
42
44
|
|
|
43
45
|
Common options:
|
|
44
46
|
--repo OWNER/REPO GitHub repository
|
|
@@ -94,6 +96,12 @@ Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. The current PR head S
|
|
|
94
96
|
'job cancel': `Cancel the durable Job and close its backing pull request.
|
|
95
97
|
|
|
96
98
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. Nonterminal Tasks are cancelled before the PR is closed.`,
|
|
99
|
+
'job pause': `Pause a durable Job without inventing a PAUSED lifecycle state.
|
|
100
|
+
|
|
101
|
+
Required: --repo OWNER/REPO --id JOB_ID. Optional: --pr NUMBER. Running worker and Planner generations are interrupted durably and resume as uncompleted work.`,
|
|
102
|
+
'job resume': `Resume a paused durable Job.
|
|
103
|
+
|
|
104
|
+
Required: --repo OWNER/REPO --id JOB_ID. Optional: --pr NUMBER. Interrupted work is requeued and normal reconciliation continues.`,
|
|
97
105
|
escalation: `Run the canonical escalation workflow for a suspected human/external blocker.\n\nRequired: --intent TEXT|--intent-file PATH and --blocked-on TEXT|--blocked-on-file PATH. Optional: --context/--context-file, --on-needs-user/--on-needs-user-file, --cwd. The executable workflow/escalation.mmd graph invokes Neo escalation, then owns the resolved / failed / needs_user branch. Only needs_user selects the block-for-user path.`,
|
|
98
106
|
status: `Show live aggregate runtime status from the running Agents Relay service.
|
|
99
107
|
|
package/dist/cli.js
CHANGED
|
@@ -41,6 +41,7 @@ import { SERVICE_DEFAULTS, ServiceEventBus, arg, hasArg, textOption, integerArg,
|
|
|
41
41
|
export { SERVICE_DEFAULTS, eventBus, executionMode, chatGptTabClosePolicy } from './cli-support.js';
|
|
42
42
|
import { workerTaskFromArgs } from './cli-worker-task.js';
|
|
43
43
|
import { taskExecutionContext } from './task-context.js';
|
|
44
|
+
import { pauseJob, resumeJob } from './job-pause.js';
|
|
44
45
|
export { ensureManagedGitHubJob } from './cli-job-create.js';
|
|
45
46
|
async function runManagedTaskCommand(action, args) {
|
|
46
47
|
const jobId = arg(args, '--id');
|
|
@@ -363,7 +364,7 @@ async function runContextCommand(action, args) {
|
|
|
363
364
|
console.log(JSON.stringify(context, null, 2));
|
|
364
365
|
}
|
|
365
366
|
async function runJobCommand(action, args) {
|
|
366
|
-
if (!['create', 'adopt', 'update', 'show', 'list', 'merge', 'unblock', 'cancel'].includes(action))
|
|
367
|
+
if (!['create', 'adopt', 'update', 'show', 'list', 'merge', 'unblock', 'cancel', 'pause', 'resume'].includes(action))
|
|
367
368
|
throw new Error(usage);
|
|
368
369
|
const repo = repository(args);
|
|
369
370
|
let createObjective;
|
|
@@ -400,6 +401,19 @@ async function runJobCommand(action, args) {
|
|
|
400
401
|
const id = arg(args, '--id');
|
|
401
402
|
if (!id && action !== 'create')
|
|
402
403
|
throw new Error('--id is required for managed local Jobs');
|
|
404
|
+
if (action === 'pause' || action === 'resume') {
|
|
405
|
+
const target = await storeFor(args);
|
|
406
|
+
if (target.job.repository !== repo)
|
|
407
|
+
throw new Error('Local Job/PR binding mismatch');
|
|
408
|
+
const prValue = arg(args, '--pr');
|
|
409
|
+
if (prValue && target.job.prNumber !== Number(prValue))
|
|
410
|
+
throw new Error('Local Job/PR binding mismatch');
|
|
411
|
+
const updated = action === 'pause' ? await pauseJob(target.store, id) : await resumeJob(target.store, id);
|
|
412
|
+
await saveLocal(target.localFile, updated);
|
|
413
|
+
await withEventBus(args, bus => publishWake(bus, updated, 'job', `Job ${id} ${action} requested`));
|
|
414
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
403
417
|
const prValue = arg(args, '--pr');
|
|
404
418
|
const prNumber = prValue ? Number(prValue) : 0;
|
|
405
419
|
if (prValue && (!Number.isInteger(prNumber) || (prNumber ?? 0) <= 0))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { tryTransitionTaskEvent } from './scheduler.js';
|
|
2
|
+
export async function pauseJob(store, jobId) {
|
|
3
|
+
const mutate = (current) => {
|
|
4
|
+
if (['COMPLETED', 'CANCELLED'].includes(current.state))
|
|
5
|
+
throw new Error(`Cannot pause terminal Job ${current.id} (${current.state})`);
|
|
6
|
+
if (current.pause)
|
|
7
|
+
return current;
|
|
8
|
+
const now = new Date().toISOString();
|
|
9
|
+
current.pause = { generation: 1, requestedAt: now };
|
|
10
|
+
for (const task of current.tasks) {
|
|
11
|
+
if (task.kind === 'planner' || ['SUCCEEDED', 'FAILED', 'CANCELLED'].includes(task.state))
|
|
12
|
+
continue;
|
|
13
|
+
if (task.executionId)
|
|
14
|
+
task.pausedExecution = { id: task.executionId, state: task.state === 'RUNNING' ? 'RUNNING' : 'QUEUED', attempt: task.attempt, at: now };
|
|
15
|
+
if (task.state === 'RUNNING' && !tryTransitionTaskEvent(task, 'pause').accepted)
|
|
16
|
+
throw new Error(`Cannot pause running Task ${task.id}`);
|
|
17
|
+
task.executionId = null;
|
|
18
|
+
task.leaseOwner = null;
|
|
19
|
+
task.leaseExpiresAt = null;
|
|
20
|
+
task.updatedAt = now;
|
|
21
|
+
}
|
|
22
|
+
for (const plan of current.plans ?? []) {
|
|
23
|
+
if (plan.status !== 'PLANNING')
|
|
24
|
+
continue;
|
|
25
|
+
if (plan.executionId)
|
|
26
|
+
plan.pausedExecutionId = plan.executionId;
|
|
27
|
+
plan.executionId = null;
|
|
28
|
+
plan.leaseOwner = null;
|
|
29
|
+
plan.leaseExpiresAt = null;
|
|
30
|
+
plan.updatedAt = now;
|
|
31
|
+
}
|
|
32
|
+
current.updatedAt = now;
|
|
33
|
+
return current;
|
|
34
|
+
};
|
|
35
|
+
if (store.mutateJob)
|
|
36
|
+
return store.mutateJob(jobId, mutate);
|
|
37
|
+
const job = await store.load(jobId);
|
|
38
|
+
const updated = mutate(job);
|
|
39
|
+
await store.saveJob(updated);
|
|
40
|
+
for (const task of updated.tasks)
|
|
41
|
+
await store.saveTask(task);
|
|
42
|
+
for (const plan of updated.plans ?? [])
|
|
43
|
+
await store.savePlan(plan);
|
|
44
|
+
return updated;
|
|
45
|
+
}
|
|
46
|
+
export async function resumeJob(store, jobId) {
|
|
47
|
+
const mutate = (current) => {
|
|
48
|
+
if (!current.pause)
|
|
49
|
+
return current;
|
|
50
|
+
const now = new Date().toISOString();
|
|
51
|
+
delete current.pause;
|
|
52
|
+
for (const task of current.tasks) {
|
|
53
|
+
if (task.pausedExecution) {
|
|
54
|
+
if (task.attempt >= task.maxAttempts)
|
|
55
|
+
task.maxAttempts = task.attempt + 1;
|
|
56
|
+
delete task.pausedExecution;
|
|
57
|
+
task.updatedAt = now;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const plan of current.plans ?? [])
|
|
61
|
+
delete plan.pausedExecutionId;
|
|
62
|
+
current.updatedAt = now;
|
|
63
|
+
return current;
|
|
64
|
+
};
|
|
65
|
+
if (store.mutateJob)
|
|
66
|
+
return store.mutateJob(jobId, mutate);
|
|
67
|
+
const job = await store.load(jobId);
|
|
68
|
+
const updated = mutate(job);
|
|
69
|
+
await store.saveJob(updated);
|
|
70
|
+
for (const task of updated.tasks)
|
|
71
|
+
await store.saveTask(task);
|
|
72
|
+
for (const plan of updated.plans ?? [])
|
|
73
|
+
await store.savePlan(plan);
|
|
74
|
+
return updated;
|
|
75
|
+
}
|
package/dist/manual-plan.js
CHANGED
|
@@ -5,6 +5,8 @@ export async function executeManualPlan(store, jobId, guidance, timeoutMs, launc
|
|
|
5
5
|
let job = await store.load(jobId);
|
|
6
6
|
if (['COMPLETED', 'CANCELLED'].includes(job.state))
|
|
7
7
|
throw new Error(`Cannot plan terminal job ${job.id} (${job.state})`);
|
|
8
|
+
if (job.pause)
|
|
9
|
+
throw new Error(`Cannot plan paused Job ${job.id}; resume it first`);
|
|
8
10
|
const active = pendingPlan(job);
|
|
9
11
|
if (active && active.executionId)
|
|
10
12
|
throw new Error(`Planning round ${active.id} is already PLANNING`);
|
|
@@ -13,6 +15,8 @@ export async function executeManualPlan(store, jobId, guidance, timeoutMs, launc
|
|
|
13
15
|
const committed = await store.mutateJob(jobId, current => {
|
|
14
16
|
if (['COMPLETED', 'CANCELLED'].includes(current.state))
|
|
15
17
|
throw new Error(`Cannot plan terminal job ${current.id} (${current.state})`);
|
|
18
|
+
if (current.pause)
|
|
19
|
+
throw new Error(`Cannot plan paused Job ${current.id}; resume it first`);
|
|
16
20
|
const concurrent = pendingPlan(current);
|
|
17
21
|
if (concurrent) {
|
|
18
22
|
if (concurrent.executionId)
|
package/dist/planner-runtime.js
CHANGED
|
@@ -281,6 +281,8 @@ export async function launchPlanExecution(ports, job, plan) {
|
|
|
281
281
|
}
|
|
282
282
|
const claimed = await withExecutionBoundary(job.id, '__job_execution_slot__', async () => {
|
|
283
283
|
const freshJob = await ports.store.load(job.id);
|
|
284
|
+
if (freshJob.pause)
|
|
285
|
+
return null;
|
|
284
286
|
const fresh = (freshJob.plans ?? []).find(item => item.id === plan.id);
|
|
285
287
|
if (!fresh || fresh.status !== 'PLANNING')
|
|
286
288
|
return null;
|
package/dist/pool.js
CHANGED
|
@@ -10,6 +10,8 @@ const RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
|
10
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); }
|
|
11
11
|
export function runnableManagedJobs(items) { return items.filter(item => item.job.state === 'ACTIVE').map(item => item.job).sort(compareJobPriority); }
|
|
12
12
|
export function needsLifecycleReconciliation(job, now = Date.now()) {
|
|
13
|
+
if (job.pause)
|
|
14
|
+
return job.tasks.some(task => Boolean(task.pausedExecution)) || Boolean(job.plans?.some(plan => Boolean(plan.pausedExecutionId)));
|
|
13
15
|
// DRAFT create-only Jobs stay dormant until a caller explicitly creates work.
|
|
14
16
|
if (job.tasks.length === 0)
|
|
15
17
|
return false;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Stop only generations already invalidated by the durable pause mutation. */
|
|
2
|
+
export function cancelPausedGenerations(job, cancelTask, cancelPlan) {
|
|
3
|
+
if (!job.pause)
|
|
4
|
+
return;
|
|
5
|
+
for (const task of job.tasks)
|
|
6
|
+
if (task.pausedExecution)
|
|
7
|
+
cancelTask(task.id, task.pausedExecution.id);
|
|
8
|
+
for (const plan of job.plans ?? [])
|
|
9
|
+
if (plan.pausedExecutionId)
|
|
10
|
+
cancelPlan(plan.id);
|
|
11
|
+
}
|
package/dist/reconciler.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cancelPausedGenerations } from './reconciler-pause.js';
|
|
1
2
|
import { taskCommentContext } from './task-comments.js';
|
|
2
3
|
import { cancelActiveTasks } from './reconciler-cancellation.js';
|
|
3
4
|
import { authorizedInjection } from './e2e-injection.js';
|
|
@@ -41,7 +42,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
41
42
|
this.workflowEffects.register(new CallbackWorkflowEffectHandler('effect.launchTask', async (context) => {
|
|
42
43
|
const job = await this.store.load(context.jobId);
|
|
43
44
|
const task = context.taskId ? job.tasks.find(item => item.id === context.taskId) : undefined;
|
|
44
|
-
if (!task || task.state !== 'QUEUED' || task.executionId !== null)
|
|
45
|
+
if (!task || job.pause || task.state !== 'QUEUED' || task.executionId !== null)
|
|
45
46
|
return;
|
|
46
47
|
if (task.kind !== 'planner' && task.adapter !== 'orchestrator')
|
|
47
48
|
await this.launch(job, task);
|
|
@@ -51,6 +52,8 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
51
52
|
}
|
|
52
53
|
async dispatchExecutableTasks(jobId) {
|
|
53
54
|
const scheduled = schedule(await this.store.load(jobId));
|
|
55
|
+
if (scheduled.pause)
|
|
56
|
+
return scheduled;
|
|
54
57
|
for (const task of scheduled.tasks.filter(item => item.state === 'SUCCEEDED' && (item.continuation || scheduled.continuation) && !item.continuationDeliveredAt)) {
|
|
55
58
|
this.detach(this.deliverContinuation(scheduled, task), scheduled.id, task.id, 'continuation');
|
|
56
59
|
}
|
|
@@ -92,6 +95,14 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
92
95
|
}
|
|
93
96
|
await this.invalidateStaleReview(job, headSha);
|
|
94
97
|
const now = (this.options.now ?? new Date()).getTime();
|
|
98
|
+
if (job.pause) {
|
|
99
|
+
cancelPausedGenerations(job, (taskId, executionId) => this.cancelExecutionGeneration(job.id, taskId, executionId), planId => { const live = this.plannerLive.get(planId); if (live) {
|
|
100
|
+
clearTimeout(live.timer);
|
|
101
|
+
live.controller.abort();
|
|
102
|
+
this.plannerLive.delete(planId);
|
|
103
|
+
} });
|
|
104
|
+
return job;
|
|
105
|
+
}
|
|
95
106
|
if (job.state !== 'ACTIVE')
|
|
96
107
|
return job;
|
|
97
108
|
await recoverExpiredExecutions(job, now, this.executionRecoveryPorts(job));
|
package/dist/scheduler.js
CHANGED
|
@@ -17,6 +17,8 @@ export function completionGateSatisfied(job) {
|
|
|
17
17
|
return (planSatisfied || legacySatisfied) && implementationTasks(job).every(task => task.state === 'SUCCEEDED');
|
|
18
18
|
}
|
|
19
19
|
export function schedule(job) {
|
|
20
|
+
if (job.pause)
|
|
21
|
+
return job;
|
|
20
22
|
reconcileTaskDependencies(job);
|
|
21
23
|
const scopedTasks = job.schedule?.currentRunSlot
|
|
22
24
|
? job.tasks.filter(task => Date.parse(task.createdAt) >= Date.parse(job.schedule?.currentRunSlot ?? ''))
|
package/package.json
CHANGED
|
@@ -178,6 +178,7 @@ leaseFailRunningMark --> leaseFailRunningClearLease
|
|
|
178
178
|
leaseFailRunningClearLease --> leaseFailRunningClearExecution
|
|
179
179
|
leaseFailRunningClearExecution --> failed
|
|
180
180
|
running -->|on: task.cancelled| cancelled
|
|
181
|
+
running -->|on: pause| queued
|
|
181
182
|
running -->|on: cancel| cancelled
|
|
182
183
|
|
|
183
184
|
autonomousJob["guard: autonomousJob"]
|