agents-relay 1.0.82 → 1.0.84

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 CHANGED
@@ -182,4 +182,6 @@ npx agents-relay task create --repo OWNER/REPO --pr 5 --id job-1 \
182
182
 
183
183
  GitHub PR state is authoritative for terminal lifecycle: a merged PR reconciles its managed job to `COMPLETED`; a closed, unmerged PR reconciles to `CANCELLED` and cannot launch queued work. The dashboard exposes `/api/jobs` and shows PR state, durable Job lifecycle, and derived runtime status separately. A merged PR reconciles lifecycle to `COMPLETED`; a closed, unmerged PR reconciles lifecycle to `CANCELLED`.
184
184
 
185
+ `adapter: orchestrator` is reserved for synchronous self-work by the exact orchestrator turn that creates the Task. Relay deliberately does not auto-start these Tasks during reconciliation. The creator must perform the work before ending the turn and terminalize the same Task with `npx agents-relay task complete --task-id <id> --summary <summary> ...`. If work should continue asynchronously or in a later turn, use a real worker adapter instead.
186
+
185
187
  For work that was completed outside the relay but must be represented truthfully in durable history, use `npx agents-relay record --task-id <id> --summary <summary> --commit <sha>`. Recorded tasks use the `orchestrator` adapter and terminal `SUCCEEDED` state; they do not pretend a shell/model worker executed the work. This is a repair/backfill mechanism—normal work should be created as a durable Task before execution. For managed jobs, the GitHub PR body is the canonical Job surface: hidden Job metadata marker plus the visible Markdown objective.
package/dist/cli.js CHANGED
@@ -28,7 +28,7 @@ Usage:
28
28
 
29
29
  Commands:
30
30
  job create|update|show|list|merge|cancel Manage durable GitHub-backed jobs
31
- task create|update|show|list|retry|cancel Manage durable tasks
31
+ task create|update|show|list|complete|retry|cancel Manage durable tasks
32
32
  context resolve Resolve shared project instructions, skills, agents, and runtimes
33
33
  init Create a local demo job or initialize a GitHub job
34
34
  record Backfill a completed task into durable history
@@ -106,6 +106,7 @@ Actions:
106
106
  show Show one Task
107
107
  list List Tasks for a Job
108
108
  retry Retry a failed or blocked Task
109
+ complete Complete an orchestrator-owned Task
109
110
  cancel Cancel a Task`,
110
111
  'task create': `Create one durable Task under an existing Job.
111
112
 
@@ -129,6 +130,9 @@ Required: --task-id ID and the normal Job storage options.`,
129
130
  'task list': `List durable Tasks for one Job.
130
131
 
131
132
  Required: the normal Job storage options.`,
133
+ 'task complete': `Complete one orchestrator-owned Task after the creating orchestrator has finished the work.
134
+
135
+ Required: --task-id ID, --summary TEXT, and the normal Job storage options. The Task must use adapter=orchestrator and be QUEUED or RUNNING. This command is for the same orchestrator turn that created the self-task; it is not a deferred handoff mechanism.`,
132
136
  'task retry': `Retry one durable Task.
133
137
 
134
138
  Required: --task-id ID and the normal Job storage options.`,
@@ -402,7 +406,7 @@ async function workerTaskFromArgs(job, args, now) {
402
406
  };
403
407
  }
404
408
  async function runTaskCommand(action, args) {
405
- if (!['create', 'update', 'show', 'list', 'retry', 'cancel'].includes(action))
409
+ if (!['create', 'update', 'show', 'list', 'complete', 'retry', 'cancel'].includes(action))
406
410
  throw new Error(usage);
407
411
  const { store, job, localFile } = await storeFor(args);
408
412
  if (action === 'list') {
@@ -437,6 +441,28 @@ async function runTaskCommand(action, args) {
437
441
  console.log(JSON.stringify(task, null, 2));
438
442
  return;
439
443
  }
444
+ if (action === 'complete') {
445
+ if (task.adapter !== 'orchestrator')
446
+ throw new Error('task complete is only valid for adapter=orchestrator self-tasks');
447
+ if (!['QUEUED', 'RUNNING'].includes(task.state))
448
+ throw new Error(`Task ${id} is ${task.state}; only QUEUED or RUNNING orchestrator tasks can be completed`);
449
+ const summary = arg(args, '--summary');
450
+ if (!summary)
451
+ throw new Error('task complete requires --summary');
452
+ if (task.state === 'QUEUED')
453
+ await transitionTaskEvent(task, 'task.started');
454
+ task.attempt = Math.max(task.attempt, 1);
455
+ task.result = { summary };
456
+ task.error = null;
457
+ task.leaseOwner = null;
458
+ task.leaseExpiresAt = null;
459
+ task.executionId = null;
460
+ await transitionTaskEvent(task, 'task.completed');
461
+ await store.saveTask(task);
462
+ await saveLocal(localFile, await store.load(job.id));
463
+ console.log(JSON.stringify(task, null, 2));
464
+ return;
465
+ }
440
466
  if (action === 'retry' || action === 'cancel') {
441
467
  if (action === 'retry')
442
468
  task.maxAttempts = extendRetryBudget(task);
@@ -187,15 +187,7 @@ export class Reconciler {
187
187
  for (const task of scheduled.tasks.filter(t => t.state === 'QUEUED' && t.executionId === null && dependencyReady(t, new Map(scheduled.tasks.map(item => [item.id, item])))).sort((a, b) => compareTaskPriority(a, b, scheduled.priority ?? 'P2')).slice(0, capacity)) {
188
188
  if (task.kind === 'planner')
189
189
  await this.launchPlanner(scheduled, task);
190
- else if (task.adapter === 'orchestrator') {
191
- await transitionTaskEvent(task, 'task.started');
192
- task.leaseOwner = null;
193
- task.leaseExpiresAt = null;
194
- task.executionId = null;
195
- await this.store.saveTask(task);
196
- await this.emit(eventFor(scheduled.id, task.id, task.parentTaskId, 'task.orchestrator_assigned', 'running', `Task ${task.id} assigned to orchestrator self`, 'orchestrator', { adapter: 'orchestrator' }, { agent: 'relay', component: 'agents-relay' }));
197
- }
198
- else
190
+ else if (task.adapter !== 'orchestrator')
199
191
  await this.launch(scheduled, task);
200
192
  }
201
193
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.82",
3
+ "version": "1.0.84",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,8 +23,9 @@ A **P0 job** (alias: **self job**) is executed directly by the current active or
23
23
 
24
24
  1. The orchestrator creates the durable job.
25
25
  2. The orchestrator immediately creates the required orchestrator-owned task(s); do not leave the P0 job idle.
26
- 3. The current orchestrator thread starts executing those tasks immediately rather than delegating them to another worker.
27
- 4. P0/self jobs do not require the normal reviewer gate unless the user explicitly asks for review.
28
- 5. After implementation and local verification, the orchestrator merges the job itself and completes normal post-merge reconciliation.
26
+ 3. The current orchestrator thread starts executing those tasks immediately rather than delegating them to another worker. Creating an `adapter: orchestrator` Task is never a handoff: the exact creator owns it for this turn.
27
+ 4. The orchestrator must terminalize every orchestrator-owned Task it created before ending the turn. It must not return to the user with one of its self-tasks left `QUEUED` or `RUNNING`; successful work uses `task complete` on that same durable Task.
28
+ 5. P0/self jobs do not require the normal reviewer gate unless the user explicitly asks for review.
29
+ 6. After implementation and local verification, the orchestrator merges the job itself and completes normal post-merge reconciliation.
29
30
 
30
31
  `job create` still only creates durable job state; the P0 workflow requires the orchestrator to follow it immediately with task creation and execution.
@@ -87,6 +87,16 @@ Task readiness is explicit and must use these semantics:
87
87
 
88
88
  Do not use `WAITING` as a Task state or durable Job lifecycle state. `WAITING` is a derived Job runtime status for an `ACTIVE` Job whose next progress depends on a future trigger, schedule slot, dependency release, or other condition.
89
89
 
90
+ ## Orchestrator self-task ownership
91
+
92
+ A Task with `adapter: orchestrator` is synchronous self-work owned by the exact orchestrator turn that creates it. It is not a durable handoff to Relay, another worker, a later reconcile cycle, or a future orchestrator thread.
93
+
94
+ - Creating an `adapter: orchestrator` Task is an execution commitment: the creating orchestrator must execute it in the current turn.
95
+ - The creating orchestrator must not end its turn while any orchestrator Task it created remains `QUEUED` or `RUNNING`; it must drive each such Task to a terminal state before returning control to the user.
96
+ - Relay must not auto-start, lease, route, or emit synthetic `task.started` / assignment evidence for orchestrator Tasks. A queued orchestrator Task means the creator has not yet terminalized its self-work.
97
+ - Successful self-work is terminalized on the same durable Task with `task complete --task-id ... --summary ...`; do not use `record` as a substitute for a Task that was created before execution. `record` remains a backfill mechanism for work that genuinely happened outside Relay.
98
+ - If work is intended to survive the current turn or be picked up asynchronously, use a real worker adapter rather than `orchestrator`.
99
+
90
100
  ## Worker lifecycle
91
101
 
92
102
  Relay owns process lifecycle. Workers own semantic task lifecycle. A worker must publish canonical `task.started` as its first execution action, then exactly one of `task.completed`, `task.failed`, `task.blocked`, or `task.cancelled`. Process launch/exit events do not substitute for worker semantic events.