agents-relay 1.0.9 → 1.0.11

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/pool.js CHANGED
@@ -21,6 +21,7 @@ export class RepositoryWorkerPool {
21
21
  makeReconciler;
22
22
  running = false;
23
23
  pending = false;
24
+ jobTargets = new Map();
24
25
  constructor(client, repository, trustedAuthors, concurrency, makeReconciler) {
25
26
  this.client = client;
26
27
  this.trustedAuthors = trustedAuthors;
@@ -30,6 +31,8 @@ export class RepositoryWorkerPool {
30
31
  }
31
32
  repositories;
32
33
  async reconcileDiscovered(discovered) {
34
+ for (const item of discovered)
35
+ this.jobTargets.set(item.job.id, { repository: item.job.repository, prNumber: item.job.prNumber });
33
36
  // Terminal PRs must be reconciled before OPEN-job scheduling. Otherwise the
34
37
  // OPEN-only runnable filter can strand durable jobs after GitHub merges/closes them.
35
38
  for (const job of terminalManagedJobs(discovered)) {
@@ -71,11 +74,25 @@ export class RepositoryWorkerPool {
71
74
  async reconcileTarget(repository, prNumber, jobId) {
72
75
  if (!this.repositories.includes(repository))
73
76
  return;
77
+ this.jobTargets.set(jobId, { repository, prNumber });
74
78
  const store = new GitHubStore(this.client, repository, prNumber, this.trustedAuthors);
75
79
  await this.makeReconciler(store, Math.max(1, this.concurrency)).reconcile(jobId);
76
80
  }
81
+ async handleTerminalEvent(event) {
82
+ const target = this.jobTargets.get(event.job_id);
83
+ if (!target || !this.repositories.includes(target.repository)) {
84
+ process.stderr.write(`warning: terminal event target unknown for job ${event.job_id}\n`);
85
+ return;
86
+ }
87
+ const store = new GitHubStore(this.client, target.repository, target.prNumber, this.trustedAuthors);
88
+ await this.makeReconciler(store, Math.max(1, this.concurrency)).handleEvent(event);
89
+ }
77
90
  async watch(bus) {
78
91
  return bus.subscribeAll(async (event) => {
92
+ if (['task.completed', 'task.failed', 'task.blocked'].includes(event.type)) {
93
+ await this.handleTerminalEvent(event);
94
+ return;
95
+ }
79
96
  if (event.type === 'job.wake' || event.type === 'github.webhook') {
80
97
  const repository = typeof event.data?.repository === 'string' ? event.data.repository : '';
81
98
  const prNumber = Number(event.data?.prNumber);
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { schedule, transitionTask } from './scheduler.js';
3
+ import { LaunchFailure } from './adapters.js';
3
4
  import { eventFor } from './events.js';
4
5
  import { parsePlannerResult, plannerInput } from './planner.js';
5
6
  const PRIORITY_RANK = { P0: 0, P1: 1, P2: 2, P3: 3 };
@@ -50,18 +51,19 @@ export class Reconciler {
50
51
  this.store = store;
51
52
  this.options = options;
52
53
  }
54
+ async handleEvent(event) {
55
+ if (['task.completed', 'task.failed', 'task.blocked'].includes(event.type)) {
56
+ await this.applyTerminalEvent(event);
57
+ return;
58
+ }
59
+ if (event.type === 'job.wake' || event.type === 'github.webhook')
60
+ await this.reconcile(event.job_id);
61
+ }
53
62
  async watch(jobId) {
54
63
  if (!this.options.eventBus)
55
64
  return async () => { };
56
65
  try {
57
- return await this.options.eventBus.subscribe(jobId, async (event) => {
58
- if (['task.completed', 'task.failed', 'task.blocked'].includes(event.type)) {
59
- await this.applyTerminalEvent(event);
60
- return;
61
- }
62
- if (event.type === 'job.wake' || event.type === 'github.webhook')
63
- await this.reconcile(jobId);
64
- });
66
+ return await this.options.eventBus.subscribe(jobId, event => this.handleEvent(event));
65
67
  }
66
68
  catch (error) {
67
69
  await this.reportTransportFailure(error);
@@ -244,7 +246,9 @@ export class Reconciler {
244
246
  this.detach(this.timeoutTask(job.id, task.id, durableExecutionId), job.id, task.id, 'worker terminal-event timeout');
245
247
  }, Math.max(1, task.timeoutMs));
246
248
  this.terminalTimers.set(task.id, terminalTimer);
247
- this.detach(execution.promise.then(result => this.runtimeSettled(job.id, task.id, durableExecutionId, result, null), error => this.runtimeSettled(job.id, task.id, durableExecutionId, null, error instanceof Error ? error.message : String(error))), job.id, task.id, 'worker runtime completion');
249
+ this.detach(execution.promise.then(result => this.runtimeSettled(job.id, task.id, durableExecutionId, result, null), error => error instanceof LaunchFailure
250
+ ? this.failLaunch(job.id, task.id, durableExecutionId, error.message, 'delivery')
251
+ : this.runtimeSettled(job.id, task.id, durableExecutionId, null, error instanceof Error ? error.message : String(error))), job.id, task.id, 'worker runtime completion');
248
252
  }
249
253
  async launchPlanner(job, task) {
250
254
  const planner = this.options.planner;
@@ -343,12 +347,12 @@ export class Reconciler {
343
347
  clearTimeout(timer);
344
348
  this.terminalTimers.delete(taskId);
345
349
  }
346
- async failLaunch(jobId, taskId, executionId, message) {
350
+ async failLaunch(jobId, taskId, executionId, message, phase = 'launch') {
347
351
  const job = await this.store.load(jobId);
348
352
  const task = job.tasks.find(item => item.id === taskId);
349
353
  if (!task || task.executionId !== executionId || task.state !== 'RUNNING')
350
354
  return;
351
- const event = eventFor(jobId, taskId, task.parentTaskId, 'task.failed', 'failed', `Worker launch failed: ${message}`, 'user', { phase: 'launch' });
355
+ const event = eventFor(jobId, taskId, task.parentTaskId, 'task.failed', 'failed', `Worker ${phase} failed: ${message}`, 'user', { phase });
352
356
  await this.applyTerminalEvent(event);
353
357
  await this.emit(event);
354
358
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -144,16 +144,13 @@ else:
144
144
  attachments = _upload_files(CFG.get("file", []))
145
145
  selector = _composer()
146
146
  before_count = len(_user_turns())
147
- fill_input(selector, CFG["prompt"], clear_first=True)
148
- if CFG["prompt"].strip() not in _composer_text(selector):
149
- info = js(f"""(() => {{
150
- const e=document.querySelector({json.dumps(selector)});
151
- const r=e.getBoundingClientRect();
152
- return {{x:r.x+r.width/2,y:r.y+r.height/2}};
153
- }})()""")
154
- click_at_xy(info["x"], info["y"])
155
- press_key("CTRL+A")
156
- type_text(CFG["prompt"])
147
+ info = js(f"""(() => {{
148
+ const e=document.querySelector({json.dumps(selector)});
149
+ const r=e.getBoundingClientRect();
150
+ return {{x:r.x+r.width/2,y:r.y+r.height/2}};
151
+ }})()""")
152
+ click_at_xy(info["x"], info["y"])
153
+ type_text(CFG["prompt"])
157
154
  if CFG["prompt"].strip() not in _composer_text(selector):
158
155
  raise RuntimeError("Temporary Chat prompt was not observed in the composer")
159
156