agents-relay 2.0.14 → 2.0.15

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.
@@ -19,7 +19,7 @@ export async function runBlockerCommand(action, args) {
19
19
  }
20
20
  if (action === 'resolve') {
21
21
  const summary = await blockerStore.resolveBlockerDependents(id, 'verified');
22
- await publishDependentUnblocked(eventBus(args), summary.unblocked, `Blocker Job ${id} resolved (verified)`);
22
+ await publishDependentUnblocked(await eventBus(args), summary.unblocked, `Blocker Job ${id} resolved (verified)`);
23
23
  console.log(JSON.stringify({ blocker: id, resolution: 'verified', ...summary }, null, 2));
24
24
  return;
25
25
  }
package/dist/cli-help.js CHANGED
@@ -179,7 +179,7 @@ Example:
179
179
 
180
180
  Required: --file PATH for local demo mode, or --repo OWNER/REPO --pr NUMBER --id JOB_ID.
181
181
 
182
- Managed GitHub jobs are reconciled by agents-relayd. The CLI publishes a job.wake request and does not launch workers; use --events nats.
182
+ Managed GitHub jobs are reconciled by agents-relayd. The CLI publishes a job.wake request and does not launch workers; NATS is auto-detected when reachable; use --events nats to require it explicitly.
183
183
 
184
184
  Example:
185
185
  npx agents-relay reconcile --repo OWNER/REPO --pr 12 --id job-1`,
@@ -192,7 +192,7 @@ Example:
192
192
  serve: `Run the dashboard and reconciliation service for a managed job.
193
193
 
194
194
  Required: --file PATH for local demo mode, or --repo OWNER/REPO --pr NUMBER --id JOB_ID.
195
- Common: --port PORT, --interval MS, --events nats.
195
+ Common: --port PORT, --interval MS, --events nats (optional; reachable NATS is auto-detected).
196
196
 
197
197
  Example:
198
198
  npx agents-relay serve --repo OWNER/REPO --pr 12 --id job-1`,
@@ -98,8 +98,32 @@ export async function relayTaskCommand(args, body) {
98
98
  throw new Error(`Agents Relay task command failed: ${await response.text() || `HTTP ${response.status}`}`);
99
99
  return await response.json();
100
100
  }
101
- export function eventBus(args) { if (arg(args, '--events') !== 'nats')
102
- return undefined; return new NatsEventBus(arg(args, '--nats-url', 'nats://127.0.0.1:4222'), arg(args, '--subject-prefix', 'neo.events.job')); }
101
+ export async function resolveEventBus(args, probe) {
102
+ const explicit = hasArg(args, '--events');
103
+ const requested = arg(args, '--events');
104
+ const natsUrl = arg(args, '--nats-url', process.env.NEO_NATS_URL || 'nats://127.0.0.1:4222');
105
+ if (explicit && requested !== 'nats')
106
+ return { bus: undefined, transport: 'none', source: 'explicit', natsUrl };
107
+ const bus = new NatsEventBus(natsUrl, arg(args, '--subject-prefix', 'neo.events.job'));
108
+ if (explicit)
109
+ return { bus, transport: 'nats', source: 'explicit', natsUrl };
110
+ const reachable = probe ? await probe(bus) : await bus.ping().then(() => true, () => false);
111
+ if (!reachable) {
112
+ await bus.close().catch(() => undefined);
113
+ return { bus: undefined, transport: 'none', source: 'unavailable', natsUrl };
114
+ }
115
+ return { bus, transport: 'nats', source: 'auto', natsUrl };
116
+ }
117
+ export function eventBusResolutionMessage(resolution) {
118
+ if (resolution.transport === 'nats')
119
+ return `event transport: nats (${resolution.source === 'auto' ? 'auto-detected' : 'explicit'})`;
120
+ return resolution.source === 'explicit' ? 'event transport: none (explicit)' : 'event transport: none (NATS unavailable)';
121
+ }
122
+ export async function eventBus(args) {
123
+ const resolution = await resolveEventBus(args);
124
+ process.stderr.write(`${eventBusResolutionMessage(resolution)}\n`);
125
+ return resolution.bus;
126
+ }
103
127
  export async function publishWake(bus, job, taskId, message) { if (!bus)
104
128
  return; try {
105
129
  await bus.publish(eventFor(job.id, taskId, null, 'job.wake', 'queued', message, 'orchestrator', { repository: job.repository, prNumber: job.prNumber }));
@@ -125,7 +149,7 @@ export async function publishDependentUnblocked(bus, targets, reason) { if (!bus
125
149
  } }
126
150
  export async function requestManagedReconciliation(bus, job) {
127
151
  if (!bus)
128
- throw new Error('Managed GitHub reconciliation requires --events nats so agents-relayd can execute the job');
152
+ throw new Error('Managed GitHub reconciliation requires a reachable NATS event bus (auto-detected when available, or explicit with --events nats)');
129
153
  try {
130
154
  await bus.publish(eventFor(job.id, 'reconcile', null, 'job.wake', 'queued', `Reconcile requested for ${job.id}`, 'orchestrator', { repository: job.repository, prNumber: job.prNumber }));
131
155
  }
@@ -7,7 +7,7 @@ export async function runTaskUnblockCommand(args, store, job, task, localFile) {
7
7
  const result = await unblockTask({ store, job, task, trigger: { source: 'cli', reason } });
8
8
  const refreshed = await store.load(job.id);
9
9
  await saveLocal(localFile, refreshed);
10
- const bus = eventBus(args);
10
+ const bus = await eventBus(args);
11
11
  await publishTaskUnblocked(bus, refreshed, task.id, reason);
12
12
  await publishWake(bus, refreshed, task.id, `Task ${task.id} unblocked${result.replanning ? ' for replanning' : ''}`);
13
13
  console.log(JSON.stringify({ taskId: task.id, ...result, task: refreshed.tasks.find(item => item.id === task.id) }, null, 2));
@@ -22,7 +22,7 @@ export async function runJobUnblockCommand(args, store, job) {
22
22
  if (job.executionMode === 'autonomous') {
23
23
  const planTrigger = await unblockAutonomousTasks({ store, job, tasks: blocked, trigger: { source: 'cli', reason } });
24
24
  const refreshed = await store.load(job.id);
25
- const bus = eventBus(args);
25
+ const bus = await eventBus(args);
26
26
  for (const task of blocked)
27
27
  await publishTaskUnblocked(bus, refreshed, task.id, reason);
28
28
  await publishWake(bus, refreshed, 'job', `Job ${job.id} unblocked for replanning`);
@@ -32,7 +32,7 @@ export async function runJobUnblockCommand(args, store, job) {
32
32
  for (const task of blocked)
33
33
  await unblockTask({ store, job, task, trigger: { source: 'cli', reason } });
34
34
  const refreshed = await store.load(job.id);
35
- const bus = eventBus(args);
35
+ const bus = await eventBus(args);
36
36
  for (const task of blocked)
37
37
  await publishTaskUnblocked(bus, refreshed, task.id, reason);
38
38
  await publishWake(bus, refreshed, 'job', `Job ${job.id} unblocked`);
package/dist/cli.js CHANGED
@@ -184,7 +184,7 @@ async function runTaskCommand(action, args) {
184
184
  }
185
185
  const durableTask = (await store.load(job.id)).tasks.find(item => item.id === task.id) ?? task;
186
186
  await saveLocal(localFile, await store.load(job.id));
187
- await publishWake(eventBus(args), refreshed, task.id, `Task ${task.id} created`);
187
+ await publishWake(await eventBus(args), refreshed, task.id, `Task ${task.id} created`);
188
188
  console.log(JSON.stringify(durableTask, null, 2));
189
189
  return;
190
190
  }
@@ -227,7 +227,7 @@ async function runTaskCommand(action, args) {
227
227
  outputTask = outputJob.tasks.find(item => item.id === id) ?? result.task;
228
228
  }
229
229
  await saveLocal(localFile, outputJob);
230
- await publishWake(eventBus(args), outputJob, outputTask.id, `${outputTask.id} ${action} requested`);
230
+ await publishWake(await eventBus(args), outputJob, outputTask.id, `${outputTask.id} ${action} requested`);
231
231
  console.log(JSON.stringify(outputTask, null, 2));
232
232
  return;
233
233
  }
@@ -395,7 +395,7 @@ async function runJobCommand(action, args) {
395
395
  await transitionJobEvent(job, 'pr.merged');
396
396
  await store.saveJob(job);
397
397
  const resolved = await store.resolveBlockerDependents(id, 'merge');
398
- await publishDependentUnblocked(eventBus(args), resolved.unblocked, `Blocker Job ${id} resolved (merge)`);
398
+ await publishDependentUnblocked(await eventBus(args), resolved.unblocked, `Blocker Job ${id} resolved (merge)`);
399
399
  console.log(JSON.stringify(await store.load(id), null, 2));
400
400
  return;
401
401
  }
@@ -465,7 +465,7 @@ export function dashboardJobAction(store, reconciler, action) {
465
465
  }
466
466
  export async function createService(args) {
467
467
  const loaded = await storeFor(args);
468
- const upstream = eventBus(args);
468
+ const upstream = await eventBus(args);
469
469
  const bus = upstream ? new ServiceEventBus(upstream) : undefined;
470
470
  const requestedUsageDays = Number(arg(args, '--usage-window-days', '1'));
471
471
  const usageWindowDays = Number.isInteger(requestedUsageDays) && requestedUsageDays > 0 ? requestedUsageDays : 1;
@@ -590,7 +590,7 @@ async function main() {
590
590
  console.log(`Initialized ${job.id}`);
591
591
  return;
592
592
  }
593
- const bus = eventBus(args);
593
+ const bus = await eventBus(args);
594
594
  if (command === 'status') {
595
595
  const host = arg(args, '--host', '127.0.0.1');
596
596
  const port = Number(arg(args, '--port', String(SERVICE_DEFAULTS.port)));
@@ -392,7 +392,8 @@ function chatShortcutText(){return localStorage.getItem(CHAT_SHORTCUTS_KEY)||DEF
392
392
  function parseChatShortcuts(source){return String(source||'').split(/\\r?\\n/).map(line=>line.trim()).filter(Boolean).map(line=>{const split=line.indexOf(':');return split>0?{name:line.slice(0,split).trim(),prompt:line.slice(split+1).trim()}:null;}).filter(item=>item&&item.name&&item.prompt);}
393
393
  function jobChatRef(job){return repoName(job?.repository)+'/pr#'+value(job?.prNumber);}
394
394
  function chatShortcutUrl(prompt){return 'https://chatgpt.com/?prompt='+encodeURIComponent(prompt);}
395
- function chatShortcutField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('ChatGPT shortcut')));const select=document.createElement('select');select.className='job-filter job-chat-shortcut-select';select.setAttribute('aria-label','ChatGPT shortcut');select.append(new Option('',''));for(const item of parseChatShortcuts(chatShortcutText()))select.append(new Option(item.name,item.prompt));select.value='';select.addEventListener('change',()=>{if(!select.value)return;const prompt=select.value.replaceAll('%1',jobChatRef(job));window.open(chatShortcutUrl(prompt),'_blank','noopener');select.value='';});box.append(select);return box;}
395
+ const CHAT_SHORTCUT_TARGET='agents-relay-shortcut';
396
+ function chatShortcutField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('ChatGPT shortcut')));const select=document.createElement('select');select.className='job-filter job-chat-shortcut-select';select.setAttribute('aria-label','ChatGPT shortcut');select.append(new Option('',''));for(const item of parseChatShortcuts(chatShortcutText()))select.append(new Option(item.name,item.prompt));select.value='';select.addEventListener('change',()=>{if(!select.value)return;const prompt=select.value.replaceAll('%1',jobChatRef(job));window.open(chatShortcutUrl(prompt),CHAT_SHORTCUT_TARGET,'noopener');select.value='';});box.append(select);return box;}
396
397
  function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected repo.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(job.id===selectedJob));row.addEventListener('click',()=>selectJob(job.id));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(job.id===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job.id);});select.append(element('span','job-title',text(repoName(job.repository)+'/PR#'+value(job.prNumber)+': '+value(job.title||job.id))));const states=element('span','job-status-line');states.append(element('span','badge '+value(job.state),text(value(job.state))),element('span','badge '+value(job.runtimeStatus||'IDLE'),text(value(job.runtimeStatus||'IDLE'))));if(job.executionMode==='autonomous')states.append(element('span','badge flag-autonomous',text('AUTONOMOUS')));if(job.reviewEnabled!==false&&job.githubState!=='MERGED')states.append(element('span','badge flag-review',text('REVIEW REQUIRED')));if(job.githubState==='MERGED'&&job.publication?.version)states.append(element('span','badge SUCCEEDED',text('v'+job.publication.version)));if(isCompleteUnmerged(job))states.append(element('span','badge MERGE_PENDING',text('MERGE PENDING')));const primary=element('div','job-primary');primary.append(states);const lastLine=element('div','job-last-line');lastLine.append(element('span','subtle',text(compactCounts(counts(job.tasks)))),element('span','subtle',text(relativeTime(job.updatedAt))));content.append(select,primary,lastLine);row.append(content);jobList.append(element('li','',row));}}
397
398
  function taskTagDisplay(job,task){const display=task.tag&&job?.tag?.values?.[task.tag];return typeof display==='string'&&display?display:null;}
398
399
  function taskSubtreeComplete(task,children){return ['SUCCEEDED','CANCELLED'].includes(task.state)&&(children.get(task.id)||[]).every(child=>taskSubtreeComplete(child,children));}
package/dist/events.js CHANGED
@@ -20,7 +20,8 @@ export class NatsEventBus {
20
20
  throw new Error(`Unsafe event subject prefix: ${subjectPrefix}`);
21
21
  }
22
22
  async moduleLoaded() { this.module ??= await import('nats'); return this.module; }
23
- async client() { const nats = await this.moduleLoaded(); this.connection ??= await nats.connect({ servers: this.url }); return this.connection; }
23
+ async client() { const nats = await this.moduleLoaded(); this.connection ??= await nats.connect({ servers: this.url, timeout: 1000 }); return this.connection; }
24
+ async ping() { await (await this.client()).flush(); }
24
25
  async publish(event) { const nats = await this.moduleLoaded(); const subject = `${this.subjectPrefix}.${subjectToken(event.job_id)}.${event.type}`; (await this.client()).publish(subject, nats.StringCodec().encode(JSON.stringify(event))); }
25
26
  async subscribe(jobId, wake) { return this.subscribeSubject(`${this.subjectPrefix}.${subjectToken(jobId)}.>`, wake); }
26
27
  async subscribeAll(wake) { return this.subscribeSubject(`${this.subjectPrefix}.>`, wake); }
package/dist/relayd.js CHANGED
@@ -6,7 +6,6 @@ import { join } from 'node:path';
6
6
  import { ChatGptAdapter, CodexAdapter } from './adapters.js';
7
7
  import { CodexThreadContinuation, CommandContinuation, WebhookContinuation } from './continuation.js';
8
8
  import { serveDashboard } from './dashboard.js';
9
- import { NatsEventBus } from './events.js';
10
9
  import { RepositoryWorkerPool, aggregateManagedGitHubJobs } from './pool.js';
11
10
  import { Reconciler } from './reconciler.js';
12
11
  import { dashboardManagedJobs, GitHubStore, InMemoryStore, loadManagedGitHubPullRequestSnapshotGraphql, withGitHubRequestContext } from './store.js';
@@ -14,6 +13,7 @@ import { ProjectedDurableStore } from './projected-store.js';
14
13
  import { discoverLocalRelayStoreSnapshots, SQLiteRelayStore } from './relay-store.js';
15
14
  import { githubAuthContext } from './github-auth.js';
16
15
  import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
16
+ import { eventBusResolutionMessage, resolveEventBus } from './cli-support.js';
17
17
  import { applyJobActivateAction } from './job-actions.js';
18
18
  import { applyOrchestratorCompleteAction } from './task-actions.js';
19
19
  import { dashboardJobSetState, dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
@@ -54,7 +54,7 @@ Options:
54
54
  --projection-ms MS GitHub projection flush interval (default: 1000)
55
55
  --execution-router-rules PATH
56
56
  Router Rules file (default: ~/.agents-relay/execution-router-rules.md)
57
- --events nats Enable event wakeups
57
+ --events nats Require NATS event wakeups (otherwise auto-detect reachable NATS)
58
58
 
59
59
  Legacy single-job mode also accepts --repo OWNER/REPO --pr NUMBER --id JOB_ID.
60
60
 
@@ -119,7 +119,9 @@ export async function runDaemon(argv) {
119
119
  const client = auth.client;
120
120
  const trusted = auth.trustedAuthors;
121
121
  const concurrency = Number(value(argv, '--concurrency', '4'));
122
- const bus = value(argv, '--events') === 'nats' ? new NatsEventBus(value(argv, '--nats-url', 'nats://127.0.0.1:4222'), value(argv, '--subject-prefix', 'neo.events.job')) : undefined;
122
+ const eventResolution = await resolveEventBus([...argv]);
123
+ const bus = eventResolution.bus;
124
+ process.stderr.write(`${eventBusResolutionMessage(eventResolution)}\n`);
123
125
  const workspaceRoot = normalized.workspaceRoot ?? defaultWorkspaceRoot();
124
126
  const discoveredRepositories = normalized.repository ? [] : await discoverWorkspaceRepositories(workspaceRoot);
125
127
  const repositories = normalized.repository ? [normalized.repository] : discoveredRepositories.map(item => item.repository);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "2.0.14",
3
+ "version": "2.0.15",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {