agents-relay 2.0.106 → 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 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))
@@ -335,7 +335,7 @@ function field(label,input){const box=element('div','detail-group');box.append(e
335
335
  function threadField(label,threadId){const box=element('div','detail-group');box.append(element('p','detail-label',text(label)));const id=String(threadId||'').trim();if(!/^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(id)){box.append(element('p','detail-value',text(id||'—')));return box;}const link=document.createElement('a');link.className='detail-value';link.href='https://chatgpt.com/c/'+encodeURIComponent(id);link.target='implementation';link.rel='noopener noreferrer';link.textContent=id.slice(0,8);link.title=id;box.append(link);return box;}function markdownField(label,input){const box=element('div','detail-group');box.append(element('p','detail-label',text(label)),markdownValue(input));return box;}
336
336
  function rememberSelection(){if(selectedJob)sessionStorage.setItem('agents-relay:selected-job',selectedJob);else sessionStorage.removeItem('agents-relay:selected-job');if(selectedTaskId)sessionStorage.setItem('agents-relay:selected-task',selectedTaskId);else sessionStorage.removeItem('agents-relay:selected-task');}
337
337
  function syncMobileJobsView(){const detail=mobileJobs.matches&&Boolean(selectedJob)&&location.hash.startsWith('#/jobs/');document.body.dataset.mobileJobsView=detail?'detail':'list';}
338
- function shortManagedId(id,repository,kind){const parts=String(repository||'').split('/');if(parts.length!==2)return String(id||'');const prefix=kind+'-'+parts[0].length+'-'+parts[0]+'-'+parts[1]+'-';const value=String(id||'');const suffix=value.startsWith(prefix)?value.slice(prefix.length):'';return /^[0-9]+$/.test(suffix)?'#'+suffix:value;}function jobSelectionKey(job){return job?String(job.repository||'')+'|'+String(job.prNumber||'')+'|'+String(job.id||''):'';}
338
+ function shortManagedId(id,repository,kind){const parts=String(repository||'').split('/');if(parts.length!==2)return String(id||'');const prefix=kind+'-'+parts[0].length+'-'+parts[0]+'-'+parts[1]+'-';const value=String(id||'');const suffix=value.startsWith(prefix)?value.slice(prefix.length):'';return /^[0-9]+$/.test(suffix)?'#'+suffix:value;}function jobSelectionKey(job){return job?String(job.repository||'')+'|'+String(job.id||''):'';}function dedupeJobs(entries){const byId=new Map();for(const job of entries){const key=jobSelectionKey(job);if(!key)continue;const existing=byId.get(key);if(!existing){byId.set(key,job);continue;}const fresh=isFreshEnough(job.updatedAt,existing.updatedAt);const preferred=fresh?job:existing;const other=fresh?existing:job;byId.set(key,{...other,...preferred,prNumber:Number(preferred.prNumber||0)>0?preferred.prNumber:other.prNumber});}return [...byId.values()];}
339
339
  function jobForSelection(selection){return jobs.find(job=>jobSelectionKey(job)===selection)||jobs.find(job=>job.id===selection)||null;}
340
340
  function selected(){return jobForSelection(selectedJob)||jobs[0]||null;}
341
341
  function counts(tasks){const out={};for(const task of Array.isArray(tasks)?tasks:[])out[task.state]=(out[task.state]||0)+1;return out;}
@@ -421,7 +421,7 @@ function renderRateLimitStatus(status){
421
421
  function runtimePill(label,value){const pill=element('span','runtime-stat');pill.append(document.createTextNode(label+' '),element('strong','',text(value)));return pill;}
422
422
  async function drawRuntimeSummary(){try{const summary=await loadJSON('/api/runtime-summary');runtimeSummary.replaceChildren(runtimePill('Active Jobs',summary.activeJobs||0),runtimePill('Running Tasks',summary.runningTasks||0),runtimePill('ChatGPT',summary.workers?.chatgpt||0),runtimePill('Codex',summary.workers?.codex||0));for(const item of Array.isArray(summary.models)?summary.models:[])runtimeSummary.append(runtimePill(item.model,item.count));}catch{runtimeSummary.replaceChildren();}}
423
423
  async function refreshStatus(){try{renderRateLimitStatus(await loadJSON('/api/status'));}catch{renderRateLimitStatus(null);}}
424
- async function refresh(force=false){jobsRefresh.disabled=true;jobsRefresh.textContent='Refreshing…';try{const discovered=await loadJSON('/api/jobs'+(force?'?refresh=1':''));jobs=Array.isArray(discovered)?discovered:[];void drawRuntimeSummary();jobs.sort(compareJobs);if(selectedJob&&!jobForSelection(selectedJob)){selectedJob=null;selectedTaskId=null;}if(!selectedJob){const match=location.hash.match(/^#\\/jobs\\/(.+)$/);const fromHash=match?decodeURIComponent(match[1]):null;const fromRoute=jobForSelection(fromHash);selectedJob=jobSelectionKey(fromRoute||jobs[0]);}rememberSelection();renderAll();void loadEventHistory();connection.textContent='';connection.setAttribute('aria-label','Live durable state connected');connection.className='status-pill ok';}catch(error){connection.textContent='';connection.setAttribute('aria-label','Durable state unavailable');connection.className='status-pill bad';jobList.replaceChildren(element('li','error-state',text('Could not load jobs: '+error.message)));}finally{jobsRefresh.disabled=false;jobsRefresh.textContent='Refresh';}}
424
+ async function refresh(force=false){jobsRefresh.disabled=true;jobsRefresh.textContent='Refreshing…';try{const discovered=await loadJSON('/api/jobs'+(force?'?refresh=1':''));jobs=dedupeJobs(Array.isArray(discovered)?discovered:[]);void drawRuntimeSummary();jobs.sort(compareJobs);if(selectedJob&&!jobForSelection(selectedJob)){selectedJob=null;selectedTaskId=null;}if(!selectedJob){const match=location.hash.match(/^#\\/jobs\\/(.+)$/);const fromHash=match?decodeURIComponent(match[1]):null;const fromRoute=jobForSelection(fromHash);selectedJob=jobSelectionKey(fromRoute||jobs[0]);}rememberSelection();renderAll();void loadEventHistory();connection.textContent='';connection.setAttribute('aria-label','Live durable state connected');connection.className='status-pill ok';}catch(error){connection.textContent='';connection.setAttribute('aria-label','Durable state unavailable');connection.className='status-pill bad';jobList.replaceChildren(element('li','error-state',text('Could not load jobs: '+error.message)));}finally{jobsRefresh.disabled=false;jobsRefresh.textContent='Refresh';}}
425
425
  function renderEventScope(){const task=eventTaskId&&selected()?.tasks?.find(candidate=>candidate.id===eventTaskId);eventsAllTasks.disabled=!task;eventsAllTasks.setAttribute('aria-pressed',String(!task));eventsScope.textContent=task?'Task '+task.id+' · newest first · 60 max':'Job-wide · newest first · 60 max';}
426
426
  function eventLabel(type){return ({'task.process.launched':'Process launched','task.started':'Worker started','task.process.exited':'Sync process exited','task.process.async_exited':'Async process exited','task.completed':'Worker completed','task.failed':'Worker failed','task.blocked':'Worker blocked','review.started':'Review started','review.approved':'Review approved','review.changes_required':'Review changes required','review.blocked':'Review blocked'})[type]||type||'Event';}
427
427
  function eventSeverity(event,level){const type=String(event?.type||'').toLowerCase(),eventLevel=String(event?.level||level||'').toLowerCase(),outcome=String(event?.outcome||event?.status||'').toLowerCase();if(eventLevel==='error'||outcome==='failed'||type.endsWith('.failed')||type==='task.failed')return 'error';if(eventLevel==='warn'||outcome==='blocked'||type.endsWith('.blocked')||type==='task.blocked'||type==='review.blocked')return 'warn';return 'normal';}
@@ -484,7 +484,7 @@ window.addEventListener('keydown',event=>{if(event.key==='Tab'&&event.altKey){ev
484
484
  function parseMessage(message){try{return JSON.parse(message.data);}catch{return null;}}
485
485
  function isFreshEnough(incoming,current){const a=Date.parse(incoming||''),b=Date.parse(current||'');return !Number.isFinite(b)||!Number.isFinite(a)||a>=b;}
486
486
  function applyStatePatch(patch){if(!patch?.jobId)return false;const job=jobs.find(candidate=>candidate.id===patch.jobId);if(!job)return false;if(patch.jobState&&isFreshEnough(patch.jobUpdatedAt,job.updatedAt)){job['state']=patch.jobState;if(patch.jobUpdatedAt)job.updatedAt=patch.jobUpdatedAt;}if(patch.taskId&&patch.taskState){const task=(job.tasks||[]).find(candidate=>candidate.id===patch.taskId);if(task&&isFreshEnough(patch.taskUpdatedAt,task.updatedAt)){task['state']=patch.taskState;if(patch.taskUpdatedAt)task.updatedAt=patch.taskUpdatedAt;}}return true;}
487
- function upsertFreshJob(incoming){if(!incoming)return;const index=jobs.findIndex(job=>jobSelectionKey(job)===jobSelectionKey(incoming));if(index<0){jobs.push(incoming);return;}if(isFreshEnough(incoming.updatedAt,jobs[index].updatedAt))jobs[index]=incoming;}
487
+ function upsertFreshJob(incoming){if(!incoming)return;const index=jobs.findIndex(job=>jobSelectionKey(job)===jobSelectionKey(incoming));if(index<0){jobs.push(incoming);return;}if(isFreshEnough(incoming.updatedAt,jobs[index].updatedAt))jobs[index]={...incoming,prNumber:Number(incoming.prNumber||0)>0?incoming.prNumber:jobs[index].prNumber};}
488
488
  const stream=new EventSource('/events');
489
489
  stream.onopen=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream connected');connection.className='status-pill ok';};
490
490
  stream.onerror=()=>{connection.textContent='';connection.setAttribute('aria-label','Event stream reconnecting');connection.className='status-pill warn';};
@@ -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
+ }
@@ -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)
@@ -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
+ }
@@ -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 ?? ''))
@@ -19,6 +19,7 @@ export const WORKFLOW_EVENT_NAMES = [
19
19
  'pr.merged',
20
20
  'queue',
21
21
  'plan.requested',
22
+ 'pause',
22
23
  'planner.failed',
23
24
  'planner.unavailable',
24
25
  'reconcile',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.106",
3
+ "version": "2.0.108",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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"]