agents-relay 1.0.138 → 1.0.139

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
@@ -28,6 +28,7 @@ export class RepositoryWorkerPool {
28
28
  reconcileLocks = new Map();
29
29
  targetedReconcileFlights = new Map();
30
30
  jobTargets = new Map();
31
+ targetSeeds = new Map();
31
32
  constructor(client, repository, trustedAuthors, concurrency, makeReconciler, isRateLimited = () => false) {
32
33
  this.client = client;
33
34
  this.trustedAuthors = trustedAuthors;
@@ -99,6 +100,11 @@ export class RepositoryWorkerPool {
99
100
  this.running = false;
100
101
  }
101
102
  }
103
+ seedTargetSnapshot(repository, prNumber, jobId, seed) {
104
+ if (!this.repositories.includes(repository))
105
+ return;
106
+ this.targetSeeds.set(`${repository}#${prNumber}:${jobId}`, seed);
107
+ }
102
108
  async reconcileTarget(repository, prNumber, jobId) {
103
109
  if (this.isRateLimited() || !this.repositories.includes(repository))
104
110
  return;
@@ -108,8 +114,11 @@ export class RepositoryWorkerPool {
108
114
  if (existing)
109
115
  return existing;
110
116
  const flight = this.reconcileExclusive(jobId, async () => {
111
- const store = new GitHubStore(this.client, repository, prNumber, this.trustedAuthors);
112
- await withGitHubRequestContext(this.client, 'pool.targeted-reconcile', () => this.makeReconciler(store, Math.max(1, this.concurrency)).reconcile(jobId));
117
+ const seedKey = `${repository}#${prNumber}:${jobId}`;
118
+ const seed = this.targetSeeds.get(seedKey) ?? null;
119
+ this.targetSeeds.delete(seedKey);
120
+ const store = new GitHubStore(this.client, repository, prNumber, this.trustedAuthors, seed);
121
+ await withGitHubRequestContext(this.client, seed ? 'pool.targeted-reconcile.seeded' : 'pool.targeted-reconcile', () => this.makeReconciler(store, Math.max(1, this.concurrency)).reconcile(jobId));
113
122
  });
114
123
  this.targetedReconcileFlights.set(key, flight);
115
124
  try {
package/dist/relayd.js CHANGED
@@ -9,7 +9,7 @@ import { serveDashboard } from './dashboard.js';
9
9
  import { NatsEventBus } from './events.js';
10
10
  import { RepositoryWorkerPool, aggregateManagedGitHubJobs } from './pool.js';
11
11
  import { Reconciler } from './reconciler.js';
12
- import { dashboardManagedJobs, GitHubStore, InMemoryStore, loadManagedGitHubPullRequestGraphql, withGitHubRequestContext } from './store.js';
12
+ import { dashboardManagedJobs, GitHubStore, InMemoryStore, loadManagedGitHubPullRequestSnapshotGraphql, withGitHubRequestContext } from './store.js';
13
13
  import { githubAuthContext } from './github-auth.js';
14
14
  import { dashboardJobAction, isEntrypoint, runtimePlanner, runtimeReviewer, startService, SERVICE_DEFAULTS } from './cli.js';
15
15
  import { dashboardTaskAction, dashboardTaskSetState } from './dashboard-actions.js';
@@ -143,21 +143,21 @@ export async function runDaemon(argv) {
143
143
  catch { /* cache is best effort */ }
144
144
  };
145
145
  const pullRequestRefreshFlights = new Map();
146
- const refreshPullRequest = async (repository, prNumber) => {
146
+ const refreshPullRequestSnapshot = async (repository, prNumber) => {
147
147
  if (client.rateLimitStatus().limited)
148
- return cachedOverviews.filter(item => item.job.repository === repository && item.job.prNumber === prNumber);
148
+ return { overviews: cachedOverviews.filter(item => item.job.repository === repository && item.job.prNumber === prNumber), seed: { prNode: {}, comments: null } };
149
149
  const key = `${repository}#${prNumber}`;
150
150
  const existing = pullRequestRefreshFlights.get(key);
151
151
  if (existing)
152
152
  return existing;
153
153
  const flight = (async () => {
154
154
  githubReadPaths.prRefreshes += 1;
155
- const refreshed = await withGitHubRequestContext(client, 'relayd.pr-refresh', () => loadManagedGitHubPullRequestGraphql(client, repository, prNumber, trusted));
155
+ const snapshot = await withGitHubRequestContext(client, 'relayd.pr-refresh', () => loadManagedGitHubPullRequestSnapshotGraphql(client, repository, prNumber, trusted));
156
156
  cachedOverviews = cachedOverviews.filter(item => item.job.repository !== repository || item.job.prNumber !== prNumber);
157
- cachedOverviews.push(...refreshed);
157
+ cachedOverviews.push(...snapshot.overviews);
158
158
  cachedOverviews = dashboardManagedJobs(cachedOverviews);
159
159
  await persistCache();
160
- return refreshed;
160
+ return snapshot;
161
161
  })();
162
162
  pullRequestRefreshFlights.set(key, flight);
163
163
  try {
@@ -168,6 +168,7 @@ export async function runDaemon(argv) {
168
168
  pullRequestRefreshFlights.delete(key);
169
169
  }
170
170
  };
171
+ const refreshPullRequest = async (repository, prNumber) => (await refreshPullRequestSnapshot(repository, prNumber)).overviews;
171
172
  const initial = cachedOverviews;
172
173
  const fallback = initial[0]?.job;
173
174
  const dashboardStore = fallback ? new GitHubStore(client, fallback.repository, fallback.prNumber, trusted) : new InMemoryStore();
@@ -206,8 +207,10 @@ export async function runDaemon(argv) {
206
207
  refresh: async (event) => {
207
208
  if (!event.repository || !event.pullRequest)
208
209
  return [];
209
- const refreshed = await refreshPullRequest(event.repository, event.pullRequest);
210
- return refreshed.map(item => item.job.id);
210
+ const snapshot = await refreshPullRequestSnapshot(event.repository, event.pullRequest);
211
+ for (const item of snapshot.overviews)
212
+ pool.seedTargetSnapshot(event.repository, event.pullRequest, item.job.id, snapshot.seed);
213
+ return snapshot.overviews.map(item => item.job.id);
211
214
  },
212
215
  wake: async (event) => {
213
216
  if (!event.repository || !event.pullRequest)
@@ -11,11 +11,17 @@ export class GitHubStore {
11
11
  commentsCache = null;
12
12
  commentsFlight = null;
13
13
  clearReadSnapshot() { this.prNodeCache = null; this.commentsCache = null; this.commentsFlight = null; }
14
- constructor(client, repository, prNumber, trustedAuthors = new Set()) {
14
+ constructor(client, repository, prNumber, trustedAuthors = new Set(), seed = null) {
15
15
  this.client = client;
16
16
  this.repository = repository;
17
17
  this.prNumber = prNumber;
18
18
  this.trustedAuthors = trustedAuthors;
19
+ if (seed) {
20
+ if (Object.keys(seed.prNode).length > 0)
21
+ this.prNodeCache = Promise.resolve(seed.prNode);
22
+ if (seed.comments)
23
+ this.commentsCache = [...seed.comments];
24
+ }
19
25
  }
20
26
  taskKey(task) { return `${this.repository}#${this.prNumber}:${task.jobId ?? ''}/${task.id}`; }
21
27
  taskKeyFromBody(body) { const task = parseTask(body); return task ? this.taskKey(task) : null; }
package/dist/store.js CHANGED
@@ -12,28 +12,39 @@ function publicationFromComments(rows, trustedAuthors) {
12
12
  });
13
13
  return allowed.map(item => parsePackagePublicationMarker(String(item.body ?? ''))).filter((item) => item !== null).at(-1) ?? null;
14
14
  }
15
- export async function loadManagedGitHubPullRequestGraphql(client, repository, prNumber, trustedAuthors) {
15
+ export async function loadManagedGitHubPullRequestSnapshotGraphql(client, repository, prNumber, trustedAuthors) {
16
16
  const [owner, name] = repository.split('/', 2);
17
17
  if (!owner || !name || !Number.isInteger(prNumber) || prNumber <= 0 || !client.graphql)
18
- return [];
19
- const query = 'query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){state isDraft merged mergedAt closedAt body comments(last:100){nodes{databaseId url author{login} body}}}}}';
18
+ return { overviews: [], seed: { prNode: {}, comments: null } };
19
+ const query = 'query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){id state isDraft merged mergedAt closedAt body headRefName headRefOid comments(last:100){totalCount nodes{id databaseId url updatedAt author{login} body}}}}}';
20
20
  const data = await client.graphql(query, { owner, name, number: prNumber });
21
21
  const repo = data.repository;
22
22
  const pr = repo?.pullRequest;
23
23
  if (!pr)
24
- return [];
25
- const comments = pr.comments?.nodes;
26
- const trusted = (Array.isArray(comments) ? comments : []).filter(item => {
27
- const row = item;
24
+ return { overviews: [], seed: { prNode: {}, comments: null } };
25
+ const commentConnection = pr.comments;
26
+ const commentRows = (Array.isArray(commentConnection?.nodes) ? commentConnection.nodes : []);
27
+ const totalCount = Number(commentConnection?.totalCount ?? commentRows.length);
28
+ const seedComments = Number.isFinite(totalCount) && totalCount <= commentRows.length ? commentRows.map(row => {
28
29
  const author = row.author;
30
+ return {
31
+ id: Number(row.databaseId ?? 0),
32
+ nodeId: typeof row.id === 'string' ? row.id : undefined,
33
+ url: typeof row.url === 'string' ? row.url : undefined,
34
+ body: String(row.body ?? ''),
35
+ updatedAt: String(row.updatedAt ?? ''),
36
+ authorLogin: typeof author?.login === 'string' ? author.login : null,
37
+ };
38
+ }) : null;
39
+ const trusted = commentRows.filter(item => {
40
+ const author = item.author;
29
41
  return typeof author?.login === 'string' && trustedAuthors.has(author.login);
30
42
  });
31
43
  const bodyJob = parseJob(typeof pr.body === 'string' ? pr.body : '');
32
44
  const commentJobs = trusted.map(item => parseJob(String(item.body ?? ''))).filter((item) => item !== null);
33
45
  const jobs = bodyJob ? [bodyJob] : commentJobs;
34
- const publication = publicationFromComments((Array.isArray(comments) ? comments : []), trustedAuthors);
35
- const tasks = trusted.map(item => {
36
- const row = item;
46
+ const publication = publicationFromComments(commentRows, trustedAuthors);
47
+ const tasks = trusted.map(row => {
37
48
  const task = parseTask(String(row.body ?? ''));
38
49
  if (!task)
39
50
  return null;
@@ -42,12 +53,18 @@ export async function loadManagedGitHubPullRequestGraphql(client, repository, pr
42
53
  }).filter((item) => item !== null);
43
54
  const state = String(pr.state ?? '').toUpperCase();
44
55
  const githubState = pr.merged === true ? 'MERGED' : state === 'CLOSED' ? 'CLOSED' : 'OPEN';
45
- return jobs.map(job => {
56
+ const overviews = jobs.map(job => {
46
57
  job.tasks = tasks.filter(task => task.jobId === job.id || (!task.jobId && jobs.length === 1));
47
58
  if (!job.description && typeof pr.body === 'string' && pr.body)
48
59
  job.description = pr.body;
60
+ if (typeof pr.headRefOid === 'string' && pr.headRefOid)
61
+ job.headSha = pr.headRefOid;
49
62
  return { job, githubState, draft: pr.isDraft === true, mergedAt: typeof pr.mergedAt === 'string' ? pr.mergedAt : null, closedAt: typeof pr.closedAt === 'string' ? pr.closedAt : null, publication };
50
63
  });
64
+ return { overviews, seed: { prNode: pr, comments: seedComments } };
65
+ }
66
+ export async function loadManagedGitHubPullRequestGraphql(client, repository, prNumber, trustedAuthors) {
67
+ return (await loadManagedGitHubPullRequestSnapshotGraphql(client, repository, prNumber, trustedAuthors)).overviews;
51
68
  }
52
69
  export async function loadManagedGitHubJobGraphql(client, repository, prNumber, jobId, trustedAuthors) {
53
70
  return (await loadManagedGitHubPullRequestGraphql(client, repository, prNumber, trustedAuthors)).find(item => item.job.id === jobId) ?? null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-relay",
3
- "version": "1.0.138",
3
+ "version": "1.0.139",
4
4
  "description": "Durable async agent jobs coordinated through GitHub pull requests",
5
5
  "type": "module",
6
6
  "bin": {