agents-relay 1.0.108 → 1.0.109

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.
@@ -3,7 +3,7 @@ import { InMemoryStore, GitHubStore } from './store.js';
3
3
  import { githubAuthContext } from './github-auth.js';
4
4
  import { NatsEventBus, eventFor } from './events.js';
5
5
  import { parseJobSchedule } from './scheduler.js';
6
- export const SERVICE_DEFAULTS = { port: 8787, watchdogMs: 300000, webhookWatchdogMs: 1800000, dashboardRefreshMs: 300000 };
6
+ export const SERVICE_DEFAULTS = { port: 8787, watchdogMs: 300000, webhookWatchdogMs: 300000, dashboardRefreshMs: 300000 };
7
7
  export class ServiceEventBus {
8
8
  upstream;
9
9
  unsubscribers = new Set();
@@ -73,11 +73,13 @@ export const dashboardHtml = `<!doctype html>
73
73
  .detail-label { margin:0; color:var(--muted); font-size:.66rem; text-transform:uppercase; letter-spacing:.04em; }
74
74
  .detail-value { margin:.12rem 0 0; font-weight:650; overflow-wrap:anywhere; }
75
75
  .section-title { margin:.8rem 0 .35rem; color:var(--muted); font-size:.72rem; letter-spacing:.05em; text-transform:uppercase; }
76
- ul.tree { display:grid; gap:.25rem; }
77
- ul.tree ul { list-style:none; margin:.2rem 0 .1rem .6rem; padding-left:.6rem; border-left:1px solid var(--border); }
78
- .task-row { width:100%; display:flex; align-items:center; gap:.4rem; min-width:0; padding:.32rem .38rem; border:0; border-radius:.35rem; background:transparent; color:inherit; font:inherit; text-align:left; cursor:pointer; }
76
+ ul.tree { display:grid; gap:.25rem; min-width:0; max-width:100%; }
77
+ ul.tree li { min-width:0; max-width:100%; }
78
+ ul.tree ul { width:calc(100% - .6rem); min-width:0; max-width:calc(100% - .6rem); list-style:none; margin:.2rem 0 .1rem .6rem; padding-left:.6rem; border-left:1px solid var(--border); }
79
+ .task-row { width:100%; max-width:100%; overflow:hidden; display:flex; align-items:center; gap:.4rem; min-width:0; padding:.32rem .38rem; border:0; border-radius:.35rem; background:transparent; color:inherit; font:inherit; text-align:left; cursor:pointer; }
79
80
  .task-row[aria-current="true"] { background:var(--accent2); }
80
- .task-id { min-width:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font: .77rem ui-monospace,SFMono-Regular,Menlo,monospace; }
81
+ .task-row .badge { flex:0 0 auto; }
82
+ .task-id { min-width:0; max-width:100%; flex:1 1 0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font: .77rem ui-monospace,SFMono-Regular,Menlo,monospace; }
81
83
  .task-actions { display:flex; justify-content:flex-end; gap:.25rem; margin-bottom:.5rem; }
82
84
  .task-link { color:var(--accent); font-weight:700; }
83
85
  .agent-graph { display:grid; gap:.45rem; margin-top:.55rem; padding:.55rem; border:1px solid var(--border); border-radius:.4rem; background:var(--surface2); overflow:auto; }
@@ -129,6 +131,25 @@ export const dashboardHtml = `<!doctype html>
129
131
  #jobs-view .panel-body { max-height:none; }
130
132
  #jobs-workspace { gap:.65rem; }
131
133
  #jobs-workspace .panel-body { max-height:none; }
134
+ #usage-view .panel-head { padding:.6rem .7rem .35rem; }
135
+ #usage-view .panel-head .subtle { display:none; }
136
+ #usage-view .panel-body { padding:.45rem .5rem .65rem; max-height:none; }
137
+ #usage-view .usage-providers { gap:.45rem; }
138
+ #usage-view .usage-provider {
139
+ grid-template-columns:1fr;
140
+ gap:.55rem;
141
+ padding:.65rem .7rem;
142
+ border-radius:.55rem;
143
+ background:var(--surface);
144
+ }
145
+ #usage-view .usage-provider h3 { font-size:.78rem; color:var(--muted); text-transform:uppercase; letter-spacing:.04em; }
146
+ #usage-view .usage-bars { gap:.7rem; }
147
+ #usage-view .usage-row { gap:.25rem; }
148
+ #usage-view .usage-label { align-items:baseline; gap:.5rem; font-size:.78rem; }
149
+ #usage-view .usage-label span:last-child { font-size:1.05rem; font-weight:800; color:var(--text); }
150
+ #usage-view .usage-track { height:.48rem; border:0; background:var(--border); }
151
+ #usage-view .usage-row .subtle { font-size:.68rem; line-height:1.25; }
152
+ #usage-view .usage-provider > .detail-value { font-size:1.2rem; font-weight:800; }
132
153
  .job-summary,.detail-groups { grid-template-columns:1fr; }
133
154
  .panel-head { padding:.55rem .6rem; }
134
155
  .panel-body { padding:.6rem; }
@@ -145,8 +166,8 @@ export const dashboardHtml = `<!doctype html>
145
166
  <nav role="tablist" aria-label="Console views">
146
167
  <button id="jobs-tab" type="button" role="tab" aria-selected="true" aria-controls="jobs-view">Jobs</button>
147
168
  <button id="usage-tab" type="button" role="tab" aria-selected="false" aria-controls="usage-view">Usage</button>
148
- <button id="rules-tab" type="button" role="tab" aria-selected="false" aria-controls="rules-view">Model Router Rules</button>
149
- <button id="worker-rules-tab" type="button" role="tab" aria-selected="false" aria-controls="worker-rules-view">Worker Router Rules</button>
169
+ <button id="rules-tab" type="button" role="tab" aria-selected="false" aria-controls="rules-view">Model Rules</button>
170
+ <button id="worker-rules-tab" type="button" role="tab" aria-selected="false" aria-controls="worker-rules-view">Worker Rules</button>
150
171
  </nav>
151
172
  </header>
152
173
  <main>
package/dist/pool.js CHANGED
@@ -24,6 +24,7 @@ export class RepositoryWorkerPool {
24
24
  makeReconciler;
25
25
  running = false;
26
26
  pending = false;
27
+ reconcileLocks = new Map();
27
28
  jobTargets = new Map();
28
29
  constructor(client, repository, trustedAuthors, concurrency, makeReconciler) {
29
30
  this.client = client;
@@ -33,14 +34,31 @@ export class RepositoryWorkerPool {
33
34
  this.repositories = [...new Set(typeof repository === 'string' ? [repository] : repository)];
34
35
  }
35
36
  repositories;
37
+ async reconcileExclusive(jobId, work) {
38
+ while (this.reconcileLocks.has(jobId))
39
+ await this.reconcileLocks.get(jobId);
40
+ let release;
41
+ const gate = new Promise(resolve => { release = resolve; });
42
+ this.reconcileLocks.set(jobId, gate);
43
+ try {
44
+ await work();
45
+ }
46
+ finally {
47
+ if (this.reconcileLocks.get(jobId) === gate)
48
+ this.reconcileLocks.delete(jobId);
49
+ release();
50
+ }
51
+ }
36
52
  async reconcileDiscovered(discovered) {
37
53
  for (const item of discovered)
38
54
  this.jobTargets.set(item.job.id, { repository: item.job.repository, prNumber: item.job.prNumber });
39
55
  // Terminal PRs must be reconciled before OPEN-job scheduling. Otherwise the
40
56
  // OPEN-only runnable filter can strand durable jobs after GitHub merges/closes them.
41
57
  for (const job of terminalManagedJobs(discovered)) {
42
- const store = new GitHubStore(this.client, job.repository, job.prNumber, this.trustedAuthors);
43
- await this.makeReconciler(store, 0).reconcile(job.id);
58
+ await this.reconcileExclusive(job.id, async () => {
59
+ const store = new GitHubStore(this.client, job.repository, job.prNumber, this.trustedAuthors);
60
+ await this.makeReconciler(store, 0).reconcile(job.id);
61
+ });
44
62
  }
45
63
  const jobs = runnableManagedJobs(discovered);
46
64
  let capacity = Math.max(1, this.concurrency);
@@ -53,7 +71,9 @@ export class RepositoryWorkerPool {
53
71
  if (!needsLifecycleReconciliation(job))
54
72
  continue;
55
73
  const allowance = Math.max(1, Math.min(capacity, ready || (expiredRunning ? 1 : 0) || 1));
56
- await this.makeReconciler(store, allowance).reconcile(job.id);
74
+ await this.reconcileExclusive(job.id, async () => {
75
+ await this.makeReconciler(store, allowance).reconcile(job.id);
76
+ });
57
77
  capacity -= allowance;
58
78
  }
59
79
  }
@@ -78,8 +98,10 @@ export class RepositoryWorkerPool {
78
98
  if (!this.repositories.includes(repository))
79
99
  return;
80
100
  this.jobTargets.set(jobId, { repository, prNumber });
81
- const store = new GitHubStore(this.client, repository, prNumber, this.trustedAuthors);
82
- await this.makeReconciler(store, Math.max(1, this.concurrency)).reconcile(jobId);
101
+ await this.reconcileExclusive(jobId, async () => {
102
+ const store = new GitHubStore(this.client, repository, prNumber, this.trustedAuthors);
103
+ await this.makeReconciler(store, Math.max(1, this.concurrency)).reconcile(jobId);
104
+ });
83
105
  }
84
106
  async handleLifecycleEvent(event) {
85
107
  const target = this.jobTargets.get(event.job_id);
package/dist/relayd.js CHANGED
@@ -129,6 +129,10 @@ export async function runDaemon(argv) {
129
129
  if (!overview)
130
130
  throw new Error(`Managed job ${jobId} not found`);
131
131
  const store = new GitHubStore(client, overview.job.repository, overview.job.prNumber, trusted);
132
+ if (action === 'reconcile') {
133
+ await pool.reconcileTarget(overview.job.repository, overview.job.prNumber, jobId);
134
+ return await store.load(jobId);
135
+ }
132
136
  return await dashboardJobAction(store, makeReconciler(store, concurrency), action)(jobId);
133
137
  };
134
138
  const workspaceTaskAction = (action) => async (jobId, taskId) => {
@@ -38,7 +38,7 @@ export function usageForProvider(snapshot, provider, now = Date.now()) {
38
38
  };
39
39
  }
40
40
  const credits = snapshot?.credits.find(item => item.id === provider)?.credits;
41
- if (credits?.available) {
41
+ if (credits) {
42
42
  const knownRemaining = credits.balances.map(item => item.remaining).filter((value) => typeof value === 'number' && Number.isFinite(value));
43
43
  if (knownRemaining.length > 0)
44
44
  return { healthy: knownRemaining.some(value => value > 0) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.108",
3
+ "version": "1.0.109",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {