agents-relay 2.0.94 → 2.0.95
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/projected-store.js +2 -4
- package/dist/projection-outbox.js +26 -0
- package/dist/relay-store.js +6 -0
- package/dist/relayd.js +26 -21
- package/package.json +1 -1
package/dist/projected-store.js
CHANGED
|
@@ -111,8 +111,7 @@ export class GitHubProjectionWriter {
|
|
|
111
111
|
observedAt: new Date().toISOString(),
|
|
112
112
|
});
|
|
113
113
|
}
|
|
114
|
-
await this.relay.
|
|
115
|
-
await this.relay.recordProjectionReceipt?.(item.operationKey, 'succeeded', item.attempts, { operation: item.operation });
|
|
114
|
+
await this.relay.completeProjectionWithReceipt(item.id, item.operationKey, 'succeeded', item.attempts, { operation: item.operation });
|
|
116
115
|
projected += 1;
|
|
117
116
|
observability().record({
|
|
118
117
|
stage: 'projection', event: 'projection.completed', outcome: 'succeeded',
|
|
@@ -137,8 +136,7 @@ export class GitHubProjectionWriter {
|
|
|
137
136
|
}
|
|
138
137
|
else {
|
|
139
138
|
const retryAt = githubProjectionRetryAt(error, item.attempts, now);
|
|
140
|
-
await this.relay.
|
|
141
|
-
await this.relay.recordProjectionReceipt?.(item.operationKey, 'failed', item.attempts + 1, { operation: item.operation, error: message });
|
|
139
|
+
await this.relay.retryProjectionWithReceipt(item.id, item.operationKey, message, retryAt, item.attempts + 1, { operation: item.operation, error: message });
|
|
142
140
|
deferred += 1;
|
|
143
141
|
observability().record({
|
|
144
142
|
stage: 'projection', event: 'projection.retry_scheduled', level: 'warn', outcome: 'failed',
|
|
@@ -215,6 +215,32 @@ export class ProjectionOutbox {
|
|
|
215
215
|
complete(id) {
|
|
216
216
|
this.db.prepare('DELETE FROM projection_outbox WHERE id=?').run(id);
|
|
217
217
|
}
|
|
218
|
+
completeWithReceipt(id, operationKey, status, attempts, detail, now) {
|
|
219
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
220
|
+
try {
|
|
221
|
+
this.recordReceipt(operationKey, status, attempts, detail, now);
|
|
222
|
+
this.db.prepare('DELETE FROM projection_outbox WHERE id=?').run(id);
|
|
223
|
+
this.db.exec('COMMIT');
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
this.db.exec('ROLLBACK');
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
retryWithReceipt(id, operationKey, error, nextAttemptAt, attempts, detail, now) {
|
|
231
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
232
|
+
try {
|
|
233
|
+
this.db.prepare(`UPDATE projection_outbox
|
|
234
|
+
SET status='pending', attempts=?, next_attempt_at=?, last_error=?, updated_at=? WHERE id=?`)
|
|
235
|
+
.run(attempts, nextAttemptAt.toISOString(), error, now, id);
|
|
236
|
+
this.recordReceipt(operationKey, 'failed', attempts, detail, now);
|
|
237
|
+
this.db.exec('COMMIT');
|
|
238
|
+
}
|
|
239
|
+
catch (caught) {
|
|
240
|
+
this.db.exec('ROLLBACK');
|
|
241
|
+
throw caught;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
218
244
|
retry(id, error, nextAttemptAt) {
|
|
219
245
|
this.db.prepare(`
|
|
220
246
|
UPDATE projection_outbox
|
package/dist/relay-store.js
CHANGED
|
@@ -372,7 +372,13 @@ export class SQLiteRelayStore {
|
|
|
372
372
|
return this.outbox.claimDue(limit, now);
|
|
373
373
|
}
|
|
374
374
|
async completeProjection(id) { this.outbox.complete(id); }
|
|
375
|
+
async completeProjectionWithReceipt(id, operationKey, status, attempts, detail) {
|
|
376
|
+
this.outbox.completeWithReceipt(id, operationKey, status, attempts, detail, new Date().toISOString());
|
|
377
|
+
}
|
|
375
378
|
async retryProjection(id, error, nextAttemptAt) { this.outbox.retry(id, error, nextAttemptAt); }
|
|
379
|
+
async retryProjectionWithReceipt(id, operationKey, error, nextAttemptAt, attempts, detail) {
|
|
380
|
+
this.outbox.retryWithReceipt(id, operationKey, error, nextAttemptAt, attempts, detail, new Date().toISOString());
|
|
381
|
+
}
|
|
376
382
|
async deadLetterProjection(id, error) { this.outbox.deadLetter(id, error); }
|
|
377
383
|
async recordProjectionConflict(id, diagnostic) { this.outbox.recordConflict(id, diagnostic); }
|
|
378
384
|
async listProjectionConflicts() { return this.outbox.listConflicts(); }
|
package/dist/relayd.js
CHANGED
|
@@ -222,12 +222,9 @@ export async function runDaemon(argv) {
|
|
|
222
222
|
const snapshot = { path: candidate.snapshotPath };
|
|
223
223
|
const discovered = candidate.job;
|
|
224
224
|
try {
|
|
225
|
-
if (discovered.state === 'CANCELLED')
|
|
226
|
-
continue;
|
|
227
225
|
if (!repositoryPaths.has(discovered.repository) || !discovered.localMerge?.sourceRef || !discovered.localMerge?.targetRef)
|
|
228
226
|
continue;
|
|
229
|
-
const sourceRef = discovered.localMerge.sourceRef;
|
|
230
|
-
const targetRef = discovered.localMerge.targetRef;
|
|
227
|
+
const sourceRef = discovered.localMerge.sourceRef, targetRef = discovered.localMerge.targetRef;
|
|
231
228
|
if (!sourceRef.startsWith('refs/heads/') || !targetRef.startsWith('refs/heads/'))
|
|
232
229
|
continue;
|
|
233
230
|
if (targetRef !== 'refs/heads/main')
|
|
@@ -250,13 +247,33 @@ export async function runDaemon(argv) {
|
|
|
250
247
|
await bus?.publish(eventFor(discovered.id, 'projection', null, 'projection.delivered', 'succeeded', `GitHub main synchronization ${published.status}`, 'orchestrator', { operation: 'github.syncMain', deliveryStatus: published.status, mergedCommitSha: published.localMergedCommitSha }, { agent: 'relay', component: 'github-projection' }));
|
|
251
248
|
continue;
|
|
252
249
|
}
|
|
253
|
-
const existingPrNumber = discovered.prNumber > 0 ? discovered.prNumber : 0;
|
|
254
250
|
const terminal = ['COMPLETED', 'CANCELLED'].includes(discovered.state);
|
|
255
251
|
const sourceSha = await gitText(canonical, ['rev-parse', '--verify', sourceRef]).catch(() => '');
|
|
256
252
|
if (!sourceSha)
|
|
257
253
|
continue;
|
|
258
254
|
let job = (await relay.loadJob(discovered.id)) ?? discovered;
|
|
259
255
|
let prNumber = job.prNumber > 0 ? job.prNumber : 0;
|
|
256
|
+
// The callback is part of the durable create intent, not daemon memory.
|
|
257
|
+
// Rebuild it on every scan so a reclaimed row remains executable after a
|
|
258
|
+
// crash, including when the binding mutation committed first.
|
|
259
|
+
const effect = async (item) => {
|
|
260
|
+
const payload = item.payload;
|
|
261
|
+
await gitText(canonical, ['fetch', '--no-tags', 'origin', `+refs/heads/${payload.base}:refs/remotes/origin/${payload.base}`]);
|
|
262
|
+
const remoteRef = `refs/remotes/origin/${payload.head}`;
|
|
263
|
+
const remoteHeadSha = await gitText(canonical, ['rev-parse', '--verify', remoteRef]).catch(() => '');
|
|
264
|
+
const remoteTreeMatchesBase = remoteHeadSha ? await gitText(canonical, ['diff', '--quiet', `refs/remotes/origin/${payload.base}`, remoteRef]).then(() => true, () => false) : false;
|
|
265
|
+
await gitText(canonical, authoritativeBranchPushArgs({ sourceRef: payload.sourceRef, branch: payload.head, remoteHeadSha: remoteHeadSha || null, remoteTreeMatchesBase }));
|
|
266
|
+
// Lookup by head/base first: a crash after GitHub created the PR must
|
|
267
|
+
// bind the existing collaboration projection rather than create one.
|
|
268
|
+
const pr = await resolvePullRequest(client, payload.repository, { head: payload.head, base: payload.base, title: payload.title, body: payload.body });
|
|
269
|
+
if (!relay.mutateJob)
|
|
270
|
+
throw new Error('Atomic SQLite mutation is required for GitHub PR binding');
|
|
271
|
+
await relay.mutateJob(job.id, current => ({ ...current, prNumber: pr.number, githubProjection: { status: 'pr_bound', sourceSha, observedAt: new Date().toISOString() }, updatedAt: new Date().toISOString() }));
|
|
272
|
+
observability().record({ stage: 'projection', event: 'github.pr_bound', outcome: 'succeeded', message: `Bound local Job ${job.id} to GitHub PR #${pr.number}`, correlation: { repository: job.repository, prNumber: pr.number, jobId: job.id }, data: { head: branch, base } });
|
|
273
|
+
};
|
|
274
|
+
projectionWriters.set(`job:${job.id}`, new GitHubProjectionWriter(relay, prNumber > 0 ? new GitHubStore(client, job.repository, prNumber, trusted) : new InMemoryStore(job), undefined, true, effect));
|
|
275
|
+
if (job.state === 'CANCELLED')
|
|
276
|
+
continue; // Drain committed effects only; never push.
|
|
260
277
|
if (prNumber === 0) {
|
|
261
278
|
// Do not bootstrap an empty remote-only PR commit: it would diverge from
|
|
262
279
|
// the real local Job branch before its first Worker commit. Publish the
|
|
@@ -277,21 +294,9 @@ export async function runDaemon(argv) {
|
|
|
277
294
|
await relay.mutateJob(job.id, current => ({ ...current, githubProjection: { status: 'merged_without_pr', sourceSha, observedAt: new Date().toISOString() } }));
|
|
278
295
|
continue;
|
|
279
296
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const remoteRef = `refs/remotes/origin/${payload.head}`;
|
|
284
|
-
const remoteHeadSha = await gitText(canonical, ['rev-parse', '--verify', remoteRef]).catch(() => '');
|
|
285
|
-
const remoteTreeMatchesBase = remoteHeadSha ? await gitText(canonical, ['diff', '--quiet', `refs/remotes/origin/${payload.base}`, remoteRef]).then(() => true, () => false) : false;
|
|
286
|
-
await gitText(canonical, authoritativeBranchPushArgs({ sourceRef: payload.sourceRef, branch: payload.head, remoteHeadSha: remoteHeadSha || null, remoteTreeMatchesBase }));
|
|
287
|
-
const pr = await resolvePullRequest(client, payload.repository, { head: payload.head, base: payload.base, title: payload.title, body: payload.body });
|
|
288
|
-
if (!relay.mutateJob)
|
|
289
|
-
throw new Error('Atomic SQLite mutation is required for GitHub PR binding');
|
|
290
|
-
await relay.mutateJob(job.id, current => ({ ...current, prNumber: pr.number, githubProjection: { status: 'pr_bound', sourceSha, observedAt: new Date().toISOString() }, updatedAt: new Date().toISOString() }));
|
|
291
|
-
observability().record({ stage: 'projection', event: 'github.pr_bound', outcome: 'succeeded', message: `Bound local Job ${job.id} to GitHub PR #${pr.number}`, correlation: { repository: job.repository, prNumber: pr.number, jobId: job.id }, data: { head: branch, base } });
|
|
292
|
-
};
|
|
293
|
-
projectionWriters.set(`job:${job.id}`, new GitHubProjectionWriter(relay, new InMemoryStore(job), undefined, true, effect));
|
|
294
|
-
if (relay.enqueueExternalActionOnce)
|
|
297
|
+
// A terminal Job may drain an already-committed intent, but terminal
|
|
298
|
+
// state is never permission to create a new external PR intent.
|
|
299
|
+
if (!terminal && relay.enqueueExternalActionOnce)
|
|
295
300
|
await relay.enqueueExternalActionOnce({ idempotencyKey: `create_pull_request:${job.id}:${sourceSha}`, operation: 'create_pull_request', payload: { operation: 'create_pull_request', repository: job.repository, head: branch, base, title: job.title, body: job.objective ?? job.description ?? '', sourceRef, targetRef }, priority: 100 });
|
|
296
301
|
if (terminal)
|
|
297
302
|
continue;
|
|
@@ -312,7 +317,7 @@ export async function runDaemon(argv) {
|
|
|
312
317
|
await relay.savePullRequestFacts({ state: latestFacts?.state === 'CLOSED' ? 'CLOSED' : 'OPEN', head: { ref: branch, sha: sourceSha }, observedAt: new Date().toISOString() });
|
|
313
318
|
}
|
|
314
319
|
const github = new GitHubStore(client, job.repository, prNumber, trusted);
|
|
315
|
-
projectionWriters.set(`job:${job.id}`, new GitHubProjectionWriter(relay, github, undefined, true));
|
|
320
|
+
projectionWriters.set(`job:${job.id}`, new GitHubProjectionWriter(relay, github, undefined, true, effect));
|
|
316
321
|
if (job.state === 'COMPLETED' && job.localMerge?.status === 'succeeded' && job.localMerge.projectionStrategy === 'target_ref_push') {
|
|
317
322
|
const injection = await consumeMethodInjection(relay, job.repository, job.id, 'github.projectLocalMerge');
|
|
318
323
|
const projectedMerge = await withTestInjection('github.projectLocalMerge', injection, () => projectAuthoritativeLocalMerge(canonical, job));
|