agents-relay 1.0.130 → 1.0.131

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/blockers.js CHANGED
@@ -11,8 +11,8 @@ export async function registerBlockerDependency(options) {
11
11
  const task = dependentJob.tasks.find(item => item.id === options.dependentTaskId);
12
12
  if (!task)
13
13
  throw new Error(`Dependent task ${options.dependentTaskId} not found`);
14
- if (!['FAILED', 'BLOCKED'].includes(task.state))
15
- throw new Error(`Dependent task ${task.id} is not blocked/failed (${task.state})`);
14
+ if (!['DRAFT', 'FAILED', 'BLOCKED'].includes(task.state))
15
+ throw new Error(`Dependent task ${task.id} is not waiting/blocked/failed (${task.state})`);
16
16
  const now = options.now ?? new Date().toISOString();
17
17
  const relation = {
18
18
  repository: dependentJob.repository,
@@ -48,7 +48,7 @@ export async function registerBlockerDependency(options) {
48
48
  function obsoleteReason(task, relation) {
49
49
  if (!task)
50
50
  return 'dependent task no longer exists';
51
- if (!['FAILED', 'BLOCKED'].includes(task.state))
51
+ if (!['DRAFT', 'FAILED', 'BLOCKED'].includes(task.state))
52
52
  return `dependent task advanced to ${task.state}`;
53
53
  if (task.attempt !== relation.blockedAttempt)
54
54
  return `dependent task advanced from attempt ${relation.blockedAttempt} to ${task.attempt}`;
@@ -81,8 +81,7 @@ export async function resolveBlockerDependencies(options) {
81
81
  continue;
82
82
  }
83
83
  const task = targetJob.tasks.find(item => item.id === relation.taskId);
84
- const blockerRef = { repository: blocker.repository, prNumber: blocker.prNumber, jobId: blocker.id };
85
- const matches = !!task && ['FAILED', 'BLOCKED'].includes(task.state) && task.attempt === relation.blockedAttempt && sameBlocker(task.blockedBy, blockerRef);
84
+ const matches = !!task && ['DRAFT', 'FAILED', 'BLOCKED'].includes(task.state) && task.attempt === relation.blockedAttempt;
86
85
  if (!matches) {
87
86
  relation.state = 'obsolete';
88
87
  relation.resolvedAt = now;
@@ -91,8 +90,13 @@ export async function resolveBlockerDependencies(options) {
91
90
  obsolete += 1;
92
91
  continue;
93
92
  }
94
- task.maxAttempts = extendRetryBudget(task);
95
- await transitionTaskEvent(task, 'retry');
93
+ if (task.state === 'DRAFT') {
94
+ await transitionTaskEvent(task, 'release');
95
+ }
96
+ else {
97
+ task.maxAttempts = extendRetryBudget(task);
98
+ await transitionTaskEvent(task, 'retry');
99
+ }
96
100
  task.error = null;
97
101
  task.leaseOwner = null;
98
102
  task.leaseExpiresAt = null;
package/dist/planner.js CHANGED
@@ -129,6 +129,33 @@ export function parsePlannerResult(value) {
129
129
  throw new Error('Planner assessment must be a string');
130
130
  if (!Array.isArray(root.next_tasks))
131
131
  throw new Error('Planner next_tasks must be an array');
132
+ const unblockKeys = new Set();
133
+ const unblocksRaw = root.unblocks === undefined ? [] : root.unblocks;
134
+ if (!Array.isArray(unblocksRaw))
135
+ throw new Error('Planner unblocks must be an array when provided');
136
+ const unblocks = unblocksRaw.map((item, index) => {
137
+ const unblock = record(item, `unblocks[${index}]`);
138
+ const repository = optionalString(unblock.repository, `unblocks[${index}].repository`);
139
+ const jobId = optionalString(unblock.jobId, `unblocks[${index}].jobId`);
140
+ const taskId = optionalString(unblock.taskId, `unblocks[${index}].taskId`);
141
+ if (!repository || !/^[^/\s]+\/[^/\s]+$/.test(repository))
142
+ throw new Error(`Planner unblocks[${index}].repository must be OWNER/REPO`);
143
+ if (!jobId)
144
+ throw new Error(`Planner unblocks[${index}].jobId is required`);
145
+ if (!taskId)
146
+ throw new Error(`Planner unblocks[${index}].taskId is required`);
147
+ const prNumber = unblock.prNumber;
148
+ if (!Number.isInteger(prNumber) || Number(prNumber) <= 0)
149
+ throw new Error(`Planner unblocks[${index}].prNumber must be a positive integer`);
150
+ const resolutionMode = unblock.resolutionMode === undefined ? undefined : String(unblock.resolutionMode);
151
+ if (resolutionMode !== undefined && resolutionMode !== 'merge' && resolutionMode !== 'verified')
152
+ throw new Error(`Planner unblocks[${index}].resolutionMode must be merge or verified`);
153
+ const key = `${repository}#${prNumber}:${jobId}:${taskId}`;
154
+ if (unblockKeys.has(key))
155
+ throw new Error(`Planner unblocks contains duplicate dependent ${key}`);
156
+ unblockKeys.add(key);
157
+ return { repository, prNumber: Number(prNumber), jobId, taskId, resolutionMode: resolutionMode };
158
+ });
132
159
  const ids = new Set();
133
160
  const next_tasks = root.next_tasks.map((item, index) => {
134
161
  const task = record(item, `next_tasks[${index}]`);
@@ -169,7 +196,7 @@ export function parsePlannerResult(value) {
169
196
  continuation: task.continuation, maxAttempts, timeoutMs
170
197
  };
171
198
  });
172
- return { objective_status: status, assessment: root.assessment, next_tasks };
199
+ return { objective_status: status, assessment: root.assessment, next_tasks, unblocks };
173
200
  }
174
201
  export function parsePlannerOutput(output) {
175
202
  const trimmed = output.trim();
@@ -491,6 +491,10 @@ export class Reconciler extends ReconcilerReviewBase {
491
491
  try {
492
492
  const result = parsePlannerResult(raw);
493
493
  const executionContext = await this.plannerContext(job, task);
494
+ if (result.unblocks.length > 0 && !this.store.registerBlockerDependent)
495
+ throw new Error('Durable store does not support Planner cross-Job unblock registration');
496
+ for (const unblock of result.unblocks)
497
+ await this.store.registerBlockerDependent?.(job.id, unblock);
494
498
  const children = result.next_tasks.map(spec => this.plannerChild(job, task, spec, executionContext));
495
499
  for (const child of children) {
496
500
  const existing = job.tasks.find(item => item.id === child.id);
@@ -499,7 +503,7 @@ export class Reconciler extends ReconcilerReviewBase {
499
503
  }
500
504
  // Persist the completed decision before exposing executable children.
501
505
  task.plannerResult = result;
502
- task.result = { summary: result.assessment, data: { objective_status: result.objective_status, next_tasks: result.next_tasks } };
506
+ task.result = { summary: result.assessment, data: { objective_status: result.objective_status, next_tasks: result.next_tasks, unblocks: result.unblocks } };
503
507
  task.error = null;
504
508
  task.plannerRetryFeedback = undefined;
505
509
  task.leaseOwner = null;
@@ -1,6 +1,6 @@
1
1
  import { parseAgent, parseJob, parseTask, renderAgentComment, renderJobBody, renderTaskComment, JOB_MARKER } from './markers.js';
2
2
  import { isTaskForJob, sameTaskDefinition } from './store-task-definition.js';
3
- import { resolveBlockerDependencies } from './blockers.js';
3
+ import { registerBlockerDependency, resolveBlockerDependencies } from './blockers.js';
4
4
  const TASK_WRITE_OVERLAY = new Map();
5
5
  export class GitHubStore {
6
6
  client;
@@ -118,6 +118,16 @@ export class GitHubStore {
118
118
  return ref && sha ? { ref, sha } : null;
119
119
  }
120
120
  async headSha() { return (await this.pullRequestHead())?.sha ?? null; }
121
+ async registerBlockerDependent(jobId, dependent) {
122
+ await registerBlockerDependency({
123
+ blockerStore: this,
124
+ blockerJobId: jobId,
125
+ dependentStore: new GitHubStore(this.client, dependent.repository, dependent.prNumber, this.trustedAuthors),
126
+ dependentJobId: dependent.jobId,
127
+ dependentTaskId: dependent.taskId,
128
+ resolutionMode: dependent.resolutionMode,
129
+ });
130
+ }
121
131
  async resolveBlockerDependents(jobId, resolution) {
122
132
  return await resolveBlockerDependencies({ blockerStore: this, blockerJobId: jobId, resolution, dependentStore: (repository, prNumber) => new GitHubStore(this.client, repository, prNumber, this.trustedAuthors) });
123
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.130",
3
+ "version": "1.0.131",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,7 +67,8 @@ Do not create separate Tasks merely for "analyze", "format", "call tool", "check
67
67
  - Do not repeat completed work or recreate an existing Task unless replacement work is explicitly needed.
68
68
  - Prefer independently verifiable Tasks with stable IDs, but do not split work solely to make Tasks smaller.
69
69
  - A Task describes what an agent must accomplish; never emit a shell command as a worker Task.
70
- - Use dependencies only when execution order actually matters.
70
+ - Use dependencies only for Task IDs inside the current Job, and only when execution order actually matters. Never encode cross-Job or cross-repository dependencies in `dependencies`.
71
+ - When the current Job is a prerequisite for another durable Job/Task, emit that dependent in top-level `unblocks`. The current blocker Job owns this authoritative reverse relation. Dependent-side `blockedBy` is optional observability metadata and is not the source of truth. Use `resolutionMode: "merge"` when blocker PR merge is sufficient; use `"verified"` when publication, deployment, runtime health, human approval, or another post-merge fact must be verified.
71
72
  - Describe each child Task precisely enough for worker-router to decide from the Task itself. Do not invent Codex/ChatGPT capability labels. workerRequirements is optional and should normally be omitted; use an explicit runtime only when the objective already requires that runtime.
72
73
  - Do not choose codex/chatgpt, provider/model, or usage probes.
73
74
  - Every child Task must declare exactly one output: `task_pr` or `file`.
@@ -77,7 +78,7 @@ Do not create separate Tasks merely for "analyze", "format", "call tool", "check
77
78
 
78
79
  ## Output
79
80
 
80
- Return exactly one JSON object with `objective_status`, `assessment`, and `next_tasks`.
81
+ Return exactly one JSON object with `objective_status`, `assessment`, `next_tasks`, and optional top-level `unblocks`.
81
82
 
82
83
  For `objective_status: "in_progress"`, `assessment` must be Markdown containing:
83
84
 
@@ -86,7 +87,7 @@ For `objective_status: "in_progress"`, `assessment` must be Markdown containing:
86
87
 
87
88
  For `objective_status: "satisfied"`, `assessment` may be a concise evidence-based completion assessment without a graph.
88
89
 
89
- Each item in next_tasks contains id, input, output, and storySize. It may also contain priority, projectName, agentName, dependencies, capabilities, workerRequirements, maxAttempts, and timeoutMs. Adapter/routing fields are selected by Relay after planning; do not invent them.
90
+ Each item in next_tasks contains id, input, output, and storySize. `unblocks`, when present, is an array of `{repository, prNumber, jobId, taskId, resolutionMode?}` records identifying Tasks that must remain waiting until the current Job resolves their blocker. It may also contain priority, projectName, agentName, dependencies, capabilities, workerRequirements, maxAttempts, and timeoutMs. Adapter/routing fields are selected by Relay after planning; do not invent them.
90
91
 
91
92
  ### Exact `next_tasks` serialization
92
93
 
@@ -105,6 +106,7 @@ A valid non-satisfied response:
105
106
  {
106
107
  "objective_status": "in_progress",
107
108
  "assessment": "```mermaid\nflowchart LR\n A[Evidence] --> B[Content package]\n```\nOne daily package is the durable Task boundary.",
109
+ "unblocks": [{"repository":"org/app","prNumber":42,"jobId":"dependent-job","taskId":"consume-output","resolutionMode":"merge"}],
108
110
  "next_tasks": [
109
111
  {
110
112
  "id": "episode-2026-09-26",
@@ -101,9 +101,9 @@ A Task with `adapter: orchestrator` is synchronous self-work owned by the exact
101
101
 
102
102
  ## Cross-Job blocker dependencies
103
103
 
104
- Task `dependencies` remain local to one Job and must contain only Task IDs from that Job. Shared runtime/tool/platform blockers use a separate durable cross-Job relation. The blocker Job owns reverse `unblocks` records identifying dependent repository, PR, Job, Task, and blocked attempt. The dependent Task may carry a lightweight `blockedBy` reference for observability and deduplication.
104
+ Task `dependencies` remain local to one Job and must contain only Task IDs from that Job. Shared runtime/tool/platform blockers use a separate durable cross-Job relation. The blocker Job owns reverse `unblocks` records identifying dependent repository, PR, Job, Task, and blocked attempt. The dependent Task may carry a lightweight `blockedBy` reference for observability and deduplication. Planner output follows the same ownership rule: when the current Job is the prerequisite, Planner declares dependents through top-level `unblocks`; Relay persists those declarations on the blocker Job via the blocker subsystem. Planner must never encode cross-Job dependencies as local `dependencies`. A dependent may be registered while its Task is still `DRAFT`, and blocker resolution releases that Task when the blocker resolves.
105
105
 
106
- Blocker resolution is generation-safe and idempotent: Relay retries a dependent only when it is still FAILED/BLOCKED on the recorded attempt and still points at that blocker. Otherwise the relation becomes obsolete. Merge-mode blockers resolve when the blocker PR is observed merged. Blockers requiring package publication, deployment, runtime health, human/external verification, or another post-merge fact use verified mode and resolve only through the explicit verified-resolution path. Existing failure evidence is preserved; retry uses the normal FAILED/BLOCKED -> QUEUED lifecycle transition.
106
+ Blocker resolution is generation-safe and idempotent: Relay releases a dependent only when it is still DRAFT/FAILED/BLOCKED on the recorded attempt. The blocker-side `unblocks` record is authoritative; dependent-side `blockedBy` may be absent. Otherwise the relation becomes obsolete. Merge-mode blockers resolve when the blocker PR is observed merged. Blockers requiring package publication, deployment, runtime health, human/external verification, or another post-merge fact use verified mode and resolve only through the explicit verified-resolution path. Existing failure evidence is preserved; DRAFT dependents use the normal release transition and FAILED/BLOCKED dependents use the normal retry transition to QUEUED.
107
107
 
108
108
  ## Worker lifecycle
109
109