agents-relay 1.0.163 → 1.0.165
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 +9 -2
- package/dist/cli-help.js +1 -1
- package/dist/cli.js +4 -1
- package/dist/reconciler.js +16 -10
- package/dist/scheduled-jobs.js +16 -0
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +1 -1
- package/skills/agents-relay/agents/troubleshooter.agent.md +2 -2
- package/skills/agents-relay/contracts/task-job-contract.md +4 -2
package/README.md
CHANGED
|
@@ -201,6 +201,13 @@ For work that was completed outside the relay but must be represented truthfully
|
|
|
201
201
|
|
|
202
202
|
### Automatic task troubleshooting
|
|
203
203
|
|
|
204
|
-
When an ACTIVE Job contains a current-run `FAILED`
|
|
204
|
+
When an ACTIVE Job contains a current-run `FAILED` task, reconciliation may create one P0-priority `troubleshooter` recovery task for that exact failed attempt. `BLOCKED` work is not troubleshooter-owned: it remains blocked until authoritative unblock transitions it to `UNBLOCKED`. The recovery task is routed to a local Codex-capable worker with repository/tool access and runs under the bundled Troubleshooter agent contract. Troubleshooter diagnosis is event-first: it must inspect the failed task's task-scoped lifecycle event history (including ordering, duplicate/missing/late events, source and phase data) before classifying root cause or retrying.
|
|
205
205
|
|
|
206
|
-
The Troubleshooter diagnoses before retrying. A task-local or transient
|
|
206
|
+
The Troubleshooter diagnoses before retrying. A task-local or transient execution cause is repaired narrowly; a shared worker/runtime/tool defect is repaired through the owning repository's P0/self-job workflow. Only after the failure cause is repaired does it retry the original FAILED task and verify durable progress. Historical failures from older scheduled autonomous runs do not spawn recovery work, and deterministic reconciliation does not duplicate a Troubleshooter task for the same failed attempt.
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
### Authoritative unblock and Planner history
|
|
210
|
+
|
|
211
|
+
Dependency release and runtime unblock are separate concepts. Dependency completion simply releases DAG work. A runtime/external blocker remains `BLOCKED` until `task unblock`, `job unblock`, or blocker-Job resolution records the blocked baseline/current evidence and emits `task.unblocked`, transitioning the Task to `UNBLOCKED`.
|
|
212
|
+
|
|
213
|
+
For autonomous Jobs, `UNBLOCKED` is historical: Relay does not retry that old Task. The unblock evidence becomes the trigger/context of the next Planner round. Every Planner round is a first-class `agents-relay:plan:v1` PR comment. Relay creates that Plan comment before generated Task comments, then updates the same comment with the Planner assessment and generated Task IDs, preserving causal order in the PR timeline. Fixed Jobs may transition `UNBLOCKED -> QUEUED` through retry after the authoritative unblock record exists. Legacy Job `changeCycle` data is accepted only as compatibility input and is not written by the current model.
|
package/dist/cli-help.js
CHANGED
|
@@ -75,7 +75,7 @@ Example:
|
|
|
75
75
|
'job merge': `Merge the backing pull request and complete the durable Job.
|
|
76
76
|
|
|
77
77
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. The current PR head SHA is pinned into the merge request.`,
|
|
78
|
-
'job unblock': `Authoritatively unblock the effective BLOCKED work Tasks in one Job.\n\nRequired: --repo OWNER/REPO --pr NUMBER --id JOB_ID --reason TEXT. Autonomous Jobs
|
|
78
|
+
'job unblock': `Authoritatively unblock the effective BLOCKED work Tasks in one Job.\n\nRequired: --repo OWNER/REPO --pr NUMBER --id JOB_ID --reason TEXT. Autonomous Jobs transition blocked work to historical UNBLOCKED state and wake Planner with unblock facts as Plan trigger/context; fixed Jobs record UNBLOCKED first and then resume according to fixed-job retry semantics.`,
|
|
79
79
|
'job cancel': `Cancel the durable Job and close its backing pull request.
|
|
80
80
|
|
|
81
81
|
Required: --repo OWNER/REPO, --pr NUMBER, and --id JOB_ID. Nonterminal Tasks are cancelled before the PR is closed.`,
|
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import { decisionMode, LayaCliDecisionEngine } from './decision.js';
|
|
|
18
18
|
import { discoverAgents } from './registry.js';
|
|
19
19
|
import { AgentObjectivePlanner, CommandObjectivePlanner } from './planner.js';
|
|
20
20
|
import { extendRetryBudget, transitionJobEvent, transitionTaskEvent } from './scheduler.js';
|
|
21
|
-
import { dueBuckets, processDueScheduledJobs, syncScheduleLabels } from './scheduled-jobs.js';
|
|
21
|
+
import { assertScheduledAutonomousRepositoryCapability, dueBuckets, processDueScheduledJobs, syncScheduleLabels } from './scheduled-jobs.js';
|
|
22
22
|
import { resolveExecutionContext } from './execution-context.js';
|
|
23
23
|
import { defaultWorkspaceRoot, resolveManagedProjectPath, resolveRepositoryProjectPath } from './workspace.js';
|
|
24
24
|
import { runBlockerCommand } from './cli-blocker.js';
|
|
@@ -35,6 +35,7 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
|
|
|
35
35
|
const store = new GitHubStore(client, repositoryName, pr.number, trustedAuthors);
|
|
36
36
|
try {
|
|
37
37
|
const existing = await store.load(options.id);
|
|
38
|
+
await assertScheduledAutonomousRepositoryCapability(client, repositoryName, existing);
|
|
38
39
|
if (existing.schedule)
|
|
39
40
|
await syncScheduleLabels(client, repositoryName, existing);
|
|
40
41
|
return { job: existing, pr };
|
|
@@ -65,6 +66,7 @@ export async function ensureManagedGitHubJob(client, repositoryName, trustedAuth
|
|
|
65
66
|
};
|
|
66
67
|
if (options.schedule)
|
|
67
68
|
job.schedule = options.schedule;
|
|
69
|
+
await assertScheduledAutonomousRepositoryCapability(client, repositoryName, job);
|
|
68
70
|
await store.saveJob(job);
|
|
69
71
|
if (job.schedule)
|
|
70
72
|
await syncScheduleLabels(client, repositoryName, job);
|
|
@@ -448,6 +450,7 @@ async function runJobCommand(action, args) {
|
|
|
448
450
|
throw new Error('--continue-kind and --continue-target must be supplied together');
|
|
449
451
|
job.continuation = next;
|
|
450
452
|
}
|
|
453
|
+
await assertScheduledAutonomousRepositoryCapability(auth.client, repo, job);
|
|
451
454
|
job.updatedAt = new Date().toISOString();
|
|
452
455
|
await store.saveJob(job);
|
|
453
456
|
if (hasArg(args, '--schedule') && job.schedule)
|
package/dist/reconciler.js
CHANGED
|
@@ -228,9 +228,17 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
async recoverLostExecutions(job, now) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (
|
|
231
|
+
const candidates = job.tasks.filter(item => item.state === 'RUNNING' || (item.state === 'QUEUED' && item.executionId !== null));
|
|
232
|
+
for (const staleTask of candidates) {
|
|
233
|
+
if (staleTask.leaseExpiresAt === null || new Date(staleTask.leaseExpiresAt).getTime() > now)
|
|
234
|
+
continue;
|
|
235
|
+
// Re-read durable state before applying lease recovery: another reconciler may have accepted a terminal event or launched a newer execution.
|
|
236
|
+
const task = (await this.store.load(job.id)).tasks.find(item => item.id === staleTask.id);
|
|
237
|
+
if (!task)
|
|
238
|
+
continue;
|
|
239
|
+
const sameGeneration = task.executionId === staleTask.executionId && task.leaseOwner === staleTask.leaseOwner && task.leaseExpiresAt === staleTask.leaseExpiresAt;
|
|
240
|
+
const recoverable = task.state === 'RUNNING' || (task.state === 'QUEUED' && task.executionId !== null);
|
|
241
|
+
if (!sameGeneration || !recoverable || task.leaseExpiresAt === null || new Date(task.leaseExpiresAt).getTime() > now)
|
|
234
242
|
continue;
|
|
235
243
|
this.live.get(task.id)?.execution.cancel();
|
|
236
244
|
const planner = this.plannerLive.get(task.id);
|
|
@@ -239,8 +247,7 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
239
247
|
planner.controller.abort();
|
|
240
248
|
this.plannerLive.delete(task.id);
|
|
241
249
|
}
|
|
242
|
-
|
|
243
|
-
await transitionTaskEvent(task, failedEvent);
|
|
250
|
+
await transitionTaskEvent(task, task.state === 'RUNNING' ? 'task.failed' : 'fail');
|
|
244
251
|
task.error = 'Lease expired';
|
|
245
252
|
task.leaseOwner = null;
|
|
246
253
|
task.leaseExpiresAt = null;
|
|
@@ -248,12 +255,11 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
248
255
|
if (task.attempt < task.maxAttempts) {
|
|
249
256
|
await transitionTaskEvent(task, 'retry');
|
|
250
257
|
await this.store.saveTask(task);
|
|
258
|
+
continue;
|
|
251
259
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'task.failed', 'failed', task.error, 'user', { reason: 'lease_expired' }));
|
|
256
|
-
}
|
|
260
|
+
task.error += ' after maximum attempts';
|
|
261
|
+
await this.store.saveTask(task);
|
|
262
|
+
await this.emit(eventFor(job.id, task.id, task.parentTaskId, 'task.failed', 'failed', task.error, 'user', { reason: 'lease_expired' }));
|
|
257
263
|
}
|
|
258
264
|
}
|
|
259
265
|
async planJob(jobId, guidance) {
|
package/dist/scheduled-jobs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GitHubStore, loadManagedGitHubPullRequestGraphql } from './store.js';
|
|
2
2
|
import { bucketStart, nextScheduleRun, parseRunBucketLabel, runBucketLabel, scheduleBucket } from './scheduler.js';
|
|
3
3
|
export const AUTONOMOUS_LABEL = 'agents-relay:autonomous';
|
|
4
|
+
export const AUTONOMOUS_SCHEDULER_WORKFLOW = '.github/workflows/autonomous-scheduler.yml';
|
|
4
5
|
function searchPath(query, page) {
|
|
5
6
|
return `search/issues?q=${encodeURIComponent(query)}&sort=created&order=asc&per_page=100&page=${page}`;
|
|
6
7
|
}
|
|
@@ -17,6 +18,21 @@ export function indexedDueQuery(repository, bucket) {
|
|
|
17
18
|
const label = runBucketLabel(bucket);
|
|
18
19
|
return `repo:${repository} is:open is:pr label:"${AUTONOMOUS_LABEL}" label:"${label}"`;
|
|
19
20
|
}
|
|
21
|
+
export async function assertScheduledAutonomousRepositoryCapability(client, repository, job) {
|
|
22
|
+
if (job.executionMode !== 'autonomous' || !job.schedule)
|
|
23
|
+
return;
|
|
24
|
+
const metadata = await client.request(`repos/${repository}`);
|
|
25
|
+
const defaultBranch = typeof metadata.default_branch === 'string' && metadata.default_branch ? metadata.default_branch : 'main';
|
|
26
|
+
try {
|
|
27
|
+
await client.request(`repos/${repository}/contents/${AUTONOMOUS_SCHEDULER_WORKFLOW}?ref=${encodeURIComponent(defaultBranch)}`);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
+
if (!/404|Not Found/i.test(message))
|
|
32
|
+
throw error;
|
|
33
|
+
throw new Error(`SCHEDULED_AUTONOMOUS_UNAVAILABLE: ${repository} default branch ${defaultBranch} must contain ${AUTONOMOUS_SCHEDULER_WORKFLOW} before creating or enabling a scheduled autonomous Job`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
20
36
|
async function issueLabels(client, repository, prNumber) {
|
|
21
37
|
const labels = [];
|
|
22
38
|
let page = 1;
|
package/package.json
CHANGED
|
@@ -115,6 +115,6 @@ Keep secrets, credentials, private prompts, and large private payloads out of du
|
|
|
115
115
|
|
|
116
116
|
### Automatic task troubleshooting
|
|
117
117
|
|
|
118
|
-
When an ACTIVE Job contains a current-run `FAILED`
|
|
118
|
+
When an ACTIVE Job contains a current-run `FAILED` task, reconciliation may create one P0-priority `troubleshooter` recovery task for that exact failed attempt. `BLOCKED` is resolved only through authoritative unblock, which records evidence and transitions the Task to `UNBLOCKED`. The recovery task is routed to a local Codex-capable worker with repository/tool access and runs under the bundled Troubleshooter agent contract. Autonomous `UNBLOCKED` Tasks are historical and feed the next Planner round instead of being retried or sent through Troubleshooter. Each Planner round is durably represented by an `agents-relay:plan:v1` PR comment created before generated Task comments and updated in place with its assessment and generated Task IDs. Troubleshooter diagnosis is event-first: it must inspect the failed task's task-scoped lifecycle event history (including ordering, duplicate/missing/late events, source and phase data) before classifying root cause or retrying.
|
|
119
119
|
|
|
120
120
|
The Troubleshooter diagnoses before retrying. A task-local or transient blocker is repaired narrowly; a shared worker/runtime/tool defect is repaired through the owning repository's P0/self-job workflow. Only after the blocker is removed does it retry the original task and verify durable progress. Historical failures from older scheduled autonomous runs do not spawn recovery work, and deterministic reconciliation does not duplicate a Troubleshooter task for the same failed attempt.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: troubleshooter
|
|
3
|
-
description: Recover
|
|
3
|
+
description: Recover FAILED Agents Relay tasks by diagnosing runtime/execution faults and repairing recoverable causes. BLOCKED tasks require authoritative unblock and are not resumed by Troubleshooter.
|
|
4
4
|
capabilities: [troubleshoot, recovery, orchestration, local_repo, iterative_tools]
|
|
5
5
|
runtime: codex
|
|
6
6
|
delegation: [p0-self-job, relay-task-retry]
|
|
@@ -9,7 +9,7 @@ restrictions: [preserve-failure-evidence, no-blind-retry, no-human-blocker-bypas
|
|
|
9
9
|
|
|
10
10
|
# Troubleshooter Agent
|
|
11
11
|
|
|
12
|
-
You are the recovery orchestrator for a
|
|
12
|
+
You are the recovery orchestrator for a FAILED Agents Relay task. Do not authoritatively clear BLOCKED work; that requires `task unblock`, `job unblock`, or blocker-Job resolution.
|
|
13
13
|
|
|
14
14
|
Your goal is not to explain the failure. Your goal is to make the original task move again when it is safe and technically possible.
|
|
15
15
|
|
|
@@ -109,7 +109,9 @@ SHA-bound final reviewer. For example, `video-director + market + style ->
|
|
|
109
109
|
review-chair -> unified revision plan` is a project pattern, not a hard-coded
|
|
110
110
|
Relay reviewer list.
|
|
111
111
|
|
|
112
|
-
A scheduled autonomous job adds cron/timezone/next-run and missed-run/concurrency policy to the trusted job marker, plus the `agents-relay:autonomous` label and one bounded time-of-day `agents-relay:next-run:*` index label. The
|
|
112
|
+
A scheduled autonomous job adds cron/timezone/next-run and missed-run/concurrency policy to the trusted job marker, plus the `agents-relay:autonomous` label and one bounded time-of-day `agents-relay:next-run:*` index label. The target repository's default branch **must** contain `.github/workflows/autonomous-scheduler.yml`; this repository-level heartbeat is a required capability, not optional deployment glue. Job creation or update that would result in an autonomous Job with a schedule must fail loudly when that workflow is absent instead of persisting a schedule that can never advance. Unscheduled autonomous Jobs and fixed Jobs do not require this workflow.
|
|
113
|
+
|
|
114
|
+
The GitHub heartbeat discovers only indexed due candidates, validates the marker's exact `nextRunAt`, claims the slot idempotently, advances the durable next run, and pulses `agents-relay:schedule-wake`. GitHub webhook delivery then wakes local Relay for planner/worker execution. While the schedule is active, the durable Job remains `ACTIVE`; between runs its derived runtime status is `WAITING`. Scheduled recurring Jobs do not enter one-shot final-review/auto-merge after each satisfied run; older-cycle failures are historical evidence and do not block a later slot.
|
|
113
115
|
|
|
114
116
|
|
|
115
117
|
## Task evidence and head identity
|
|
@@ -122,7 +124,7 @@ This does not weaken immutable review semantics: final review and specialist rev
|
|
|
122
124
|
|
|
123
125
|
## Dependency vs block and authoritative unblock
|
|
124
126
|
|
|
125
|
-
`dependencies` describe normal DAG ordering. Dependency completion does not create an unblock event or
|
|
127
|
+
`dependencies` describe normal DAG ordering. Dependency completion does not create an unblock lifecycle event or Planner unblock trigger; Relay simply releases dependent work when its prerequisites succeed. `BLOCKED` is different: it records that execution reached a runtime/external condition that invalidates normal progress.
|
|
126
128
|
|
|
127
129
|
A BLOCKED Task cannot be resumed through retry, queue, dashboard state editing, PR push, webhook, ordinary task update, or troubleshooter inference. Except for blocker-Job resolution, the authoritative entry points are `task unblock --reason ...` and `job unblock --reason ...`. Authoritative unblock records the blocked baseline and current evidence, then transitions `BLOCKED -> UNBLOCKED` through the durable `task.unblocked` lifecycle event. `BLOCKED` therefore always means the blocker is still effective; `UNBLOCKED` means the old blocked attempt is historical and its blocker was explicitly cleared.
|
|
128
130
|
|