agents-relay 1.0.116 → 1.0.118
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/reconciler-runtime.js +28 -3
- package/dist/reconciler.js +2 -1
- package/dist/store-github.js +52 -5
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import { workflowEffectFailureEvent } from './workflow/contracts.js';
|
|
|
7
7
|
export function executionEventSource(jobId, taskId, executionId) { return `job/${jobId}/task/${taskId}/execution/${executionId}`; }
|
|
8
8
|
function eventSourceId(event) { return typeof event.source?.id === 'string' ? event.source.id : undefined; }
|
|
9
9
|
const terminalExecutionClaims = new Set();
|
|
10
|
+
const activeExecutionOwners = new Map();
|
|
10
11
|
function claimTerminalExecution(sourceId) {
|
|
11
12
|
terminalExecutionClaims.add(sourceId);
|
|
12
13
|
const cleanup = setTimeout(() => terminalExecutionClaims.delete(sourceId), 60000);
|
|
@@ -61,6 +62,12 @@ export class ReconcilerRuntimeBase {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
async handleEvent(event) {
|
|
65
|
+
const sourceId = eventSourceId(event);
|
|
66
|
+
const activeOwner = sourceId ? activeExecutionOwners.get(sourceId) : undefined;
|
|
67
|
+
if (activeOwner && activeOwner !== this) {
|
|
68
|
+
await activeOwner.handleEvent(event);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
64
71
|
if (event.type === 'task.started') {
|
|
65
72
|
await this.markWorkerStarted(event);
|
|
66
73
|
return;
|
|
@@ -87,10 +94,13 @@ export class ReconcilerRuntimeBase {
|
|
|
87
94
|
return async () => { };
|
|
88
95
|
}
|
|
89
96
|
}
|
|
90
|
-
async idle() { while (this.live.
|
|
97
|
+
async idle() { while ([...this.live.values()].some(item => !item.processSettled) || this.plannerLive.size > 0 || this.continuationLive.size > 0 || this.reconciling)
|
|
91
98
|
await new Promise(resolve => setTimeout(resolve, 25)); }
|
|
92
99
|
async close() {
|
|
93
100
|
for (const live of this.live.values()) {
|
|
101
|
+
const sourceId = executionEventSource(live.jobId, live.taskId, live.executionId);
|
|
102
|
+
if (activeExecutionOwners.get(sourceId) === this)
|
|
103
|
+
activeExecutionOwners.delete(sourceId);
|
|
94
104
|
live.controller.abort();
|
|
95
105
|
live.execution.cancel();
|
|
96
106
|
}
|
|
@@ -144,10 +154,19 @@ export class ReconcilerRuntimeBase {
|
|
|
144
154
|
clearTimeout(timer); await unsubscribe(); };
|
|
145
155
|
return { ack, stop };
|
|
146
156
|
}
|
|
157
|
+
registerExecutionOwner(jobId, taskId, executionId) {
|
|
158
|
+
activeExecutionOwners.set(executionEventSource(jobId, taskId, executionId), this);
|
|
159
|
+
}
|
|
147
160
|
clearExecution(taskId, executionId) {
|
|
148
161
|
const live = this.live.get(taskId);
|
|
149
|
-
if (!executionId || live?.executionId === executionId)
|
|
162
|
+
if (!executionId || live?.executionId === executionId) {
|
|
163
|
+
if (live) {
|
|
164
|
+
const sourceId = executionEventSource(live.jobId, live.taskId, live.executionId);
|
|
165
|
+
if (activeExecutionOwners.get(sourceId) === this)
|
|
166
|
+
activeExecutionOwners.delete(sourceId);
|
|
167
|
+
}
|
|
150
168
|
this.live.delete(taskId);
|
|
169
|
+
}
|
|
151
170
|
const timer = this.terminalTimers.get(taskId);
|
|
152
171
|
if (timer)
|
|
153
172
|
clearTimeout(timer);
|
|
@@ -228,7 +247,7 @@ export class ReconcilerRuntimeBase {
|
|
|
228
247
|
const live = this.live.get(taskId);
|
|
229
248
|
const liveWorkerStarted = live?.executionId === executionId && live.workerStarted;
|
|
230
249
|
if (live?.executionId === executionId)
|
|
231
|
-
|
|
250
|
+
live.processSettled = true;
|
|
232
251
|
const job = await this.store.load(jobId);
|
|
233
252
|
const task = job.tasks.find(item => item.id === taskId);
|
|
234
253
|
const workerStarted = liveWorkerStarted || task?.state === 'RUNNING';
|
|
@@ -249,6 +268,12 @@ export class ReconcilerRuntimeBase {
|
|
|
249
268
|
async failUnexpectedExit(jobId, taskId, executionId, workerStarted) {
|
|
250
269
|
if (terminalExecutionClaimed(jobId, taskId, executionId))
|
|
251
270
|
return;
|
|
271
|
+
const current = await this.store.load(jobId);
|
|
272
|
+
const currentTask = current.tasks.find(item => item.id === taskId);
|
|
273
|
+
if (!currentTask || currentTask.executionId !== executionId || ['SUCCEEDED', 'FAILED', 'BLOCKED', 'CANCELLED'].includes(currentTask.state)) {
|
|
274
|
+
this.clearExecution(taskId, executionId);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
252
277
|
const reason = workerStarted ? 'worker_exit_without_terminal_event' : 'worker_exit_before_start';
|
|
253
278
|
const message = workerStarted
|
|
254
279
|
? `Worker ${taskId} exited after task.started without publishing a terminal task event`
|
package/dist/reconciler.js
CHANGED
|
@@ -419,7 +419,8 @@ export class Reconciler extends ReconcilerReviewBase {
|
|
|
419
419
|
void (execution.launched ?? Promise.resolve()).then(() => this.emit(eventFor(job.id, task.id, task.parentTaskId, 'task.process.launched', 'running', `Task ${task.id} process launched`, 'user', { adapter: adapter.id, model: task.routing?.model }, { agent: 'relay', component: 'agents-relay' }))).catch(error => this.failLaunch(job.id, task.id, durableExecutionId, error instanceof Error ? error.message : String(error), 'launch'));
|
|
420
420
|
if (startAckSetup)
|
|
421
421
|
void startAckSetup.ack.finally(() => { void startAckSetup.stop(); }).catch(() => { });
|
|
422
|
-
this.live.set(task.id, { jobId: job.id, taskId: task.id, executionId: durableExecutionId, execution, controller, launchBarrier: Date.now(), workerStarted: false });
|
|
422
|
+
this.live.set(task.id, { jobId: job.id, taskId: task.id, executionId: durableExecutionId, execution, controller, launchBarrier: Date.now(), workerStarted: false, processSettled: false });
|
|
423
|
+
this.registerExecutionOwner(job.id, task.id, durableExecutionId);
|
|
423
424
|
const terminalTimer = setTimeout(() => {
|
|
424
425
|
this.detach(this.timeoutTask(job.id, task.id, durableExecutionId), job.id, task.id, 'worker terminal-event timeout');
|
|
425
426
|
}, Math.max(1, task.timeoutMs));
|
package/dist/store-github.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseAgent, parseJob, parseTask, renderAgentComment, renderJobBody, renderTaskComment, JOB_MARKER } from './markers.js';
|
|
2
2
|
import { isTaskForJob, sameTaskDefinition } from './store-task-definition.js';
|
|
3
3
|
import { resolveBlockerDependencies } from './blockers.js';
|
|
4
|
+
const TASK_WRITE_OVERLAY = new Map();
|
|
4
5
|
export class GitHubStore {
|
|
5
6
|
client;
|
|
6
7
|
repository;
|
|
@@ -12,6 +13,29 @@ export class GitHubStore {
|
|
|
12
13
|
this.prNumber = prNumber;
|
|
13
14
|
this.trustedAuthors = trustedAuthors;
|
|
14
15
|
}
|
|
16
|
+
taskKey(task) { return `${this.repository}#${this.prNumber}:${task.jobId ?? ''}/${task.id}`; }
|
|
17
|
+
taskKeyFromBody(body) { const task = parseTask(body); return task ? this.taskKey(task) : null; }
|
|
18
|
+
overlayTaskWrites(comments) {
|
|
19
|
+
if (TASK_WRITE_OVERLAY.size === 0)
|
|
20
|
+
return comments;
|
|
21
|
+
const result = [...comments];
|
|
22
|
+
for (const [key, overlay] of [...TASK_WRITE_OVERLAY]) {
|
|
23
|
+
const index = result.findIndex(comment => this.taskKeyFromBody(comment.body) === key);
|
|
24
|
+
if (index >= 0) {
|
|
25
|
+
const remote = result[index];
|
|
26
|
+
if (remote.updatedAt && overlay.updatedAt && remote.updatedAt >= overlay.updatedAt) {
|
|
27
|
+
TASK_WRITE_OVERLAY.delete(key);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
result[index] = { ...remote, ...overlay, id: remote.id || overlay.id, nodeId: remote.nodeId ?? overlay.nodeId, url: remote.url ?? overlay.url, authorLogin: remote.authorLogin ?? overlay.authorLogin };
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
TASK_WRITE_OVERLAY.delete(key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
rememberTaskWrite(task, comment, body) { TASK_WRITE_OVERLAY.set(this.taskKey(task), { ...comment, body }); }
|
|
15
39
|
isTrusted(comment) { return comment.authorLogin !== null && this.trustedAuthors.has(comment.authorLogin); }
|
|
16
40
|
repoParts() {
|
|
17
41
|
const [owner, name] = this.repository.split('/', 2);
|
|
@@ -61,7 +85,7 @@ export class GitHubStore {
|
|
|
61
85
|
const pageInfo = comments?.pageInfo;
|
|
62
86
|
cursor = pageInfo?.hasNextPage === true && typeof pageInfo.endCursor === 'string' ? pageInfo.endCursor : null;
|
|
63
87
|
} while (cursor);
|
|
64
|
-
return all;
|
|
88
|
+
return this.overlayTaskWrites(all);
|
|
65
89
|
}
|
|
66
90
|
async comments() { return this.list(); }
|
|
67
91
|
async pullRequestState() {
|
|
@@ -151,12 +175,31 @@ export class GitHubStore {
|
|
|
151
175
|
async listAgents() { return (await this.list()).filter(comment => this.isTrusted(comment)).map(x => parseAgent(x.body)).filter((x) => x !== null); }
|
|
152
176
|
async comment(body) {
|
|
153
177
|
const pr = await this.prNode();
|
|
154
|
-
await this.graphql('mutation($subjectId:ID!,$body:String!){addComment(input:{subjectId:$subjectId,body:$body}){commentEdge{node{id}}}}', { subjectId: pr.id, body });
|
|
178
|
+
const data = await this.graphql('mutation($subjectId:ID!,$body:String!){addComment(input:{subjectId:$subjectId,body:$body}){commentEdge{node{id databaseId url body updatedAt author{login}}}}}', { subjectId: pr.id, body });
|
|
179
|
+
const edge = data.addComment?.commentEdge;
|
|
180
|
+
const node = edge?.node;
|
|
181
|
+
return {
|
|
182
|
+
id: Number(node?.databaseId ?? 0),
|
|
183
|
+
nodeId: typeof node?.id === 'string' ? node.id : undefined,
|
|
184
|
+
url: typeof node?.url === 'string' ? node.url : undefined,
|
|
185
|
+
body: typeof node?.body === 'string' ? node.body : body,
|
|
186
|
+
updatedAt: typeof node?.updatedAt === 'string' ? node.updatedAt : new Date().toISOString(),
|
|
187
|
+
authorLogin: typeof node?.author?.login === 'string' ? String((node?.author).login) : [...this.trustedAuthors][0] ?? null,
|
|
188
|
+
};
|
|
155
189
|
}
|
|
156
190
|
async updateComment(comment, body) {
|
|
157
191
|
if (!comment.nodeId)
|
|
158
192
|
throw new Error('GitHub comment node id is required for GraphQL update');
|
|
159
|
-
await this.graphql('mutation($id:ID!,$body:String!){updateIssueComment(input:{id:$id,body:$body}){issueComment{id}}}', { id: comment.nodeId, body });
|
|
193
|
+
const data = await this.graphql('mutation($id:ID!,$body:String!){updateIssueComment(input:{id:$id,body:$body}){issueComment{id databaseId url body updatedAt author{login}}}}', { id: comment.nodeId, body });
|
|
194
|
+
const node = data.updateIssueComment?.issueComment;
|
|
195
|
+
return {
|
|
196
|
+
id: Number(node?.databaseId ?? comment.id),
|
|
197
|
+
nodeId: typeof node?.id === 'string' ? node.id : comment.nodeId,
|
|
198
|
+
url: typeof node?.url === 'string' ? node.url : comment.url,
|
|
199
|
+
body: typeof node?.body === 'string' ? node.body : body,
|
|
200
|
+
updatedAt: typeof node?.updatedAt === 'string' ? node.updatedAt : new Date().toISOString(),
|
|
201
|
+
authorLogin: typeof node?.author?.login === 'string' ? String((node?.author).login) : comment.authorLogin,
|
|
202
|
+
};
|
|
160
203
|
}
|
|
161
204
|
async deleteComment(comment) {
|
|
162
205
|
if (!comment.nodeId)
|
|
@@ -190,7 +233,9 @@ export class GitHubStore {
|
|
|
190
233
|
return existing !== null && existing.jobId === task.jobId && existing.id === task.id;
|
|
191
234
|
});
|
|
192
235
|
if (matches.length === 0) {
|
|
193
|
-
|
|
236
|
+
const body = renderTaskComment(task);
|
|
237
|
+
const persisted = await this.comment(body);
|
|
238
|
+
this.rememberTaskWrite(task, persisted, body);
|
|
194
239
|
return;
|
|
195
240
|
}
|
|
196
241
|
const parsed = matches.map(comment => ({ comment, task: parseTask(comment.body) }));
|
|
@@ -203,7 +248,9 @@ export class GitHubStore {
|
|
|
203
248
|
const comments = (await this.list()).filter(comment => this.isTrusted(comment));
|
|
204
249
|
const existing = comments.filter(comment => { const markerTask = parseTask(comment.body); return markerTask !== null && markerTask.jobId === task.jobId && markerTask.id === task.id; });
|
|
205
250
|
if (existing.length > 0) {
|
|
206
|
-
|
|
251
|
+
const body = renderTaskComment(task);
|
|
252
|
+
const persisted = await this.updateComment(existing[0], body);
|
|
253
|
+
this.rememberTaskWrite(task, persisted, body);
|
|
207
254
|
for (const duplicate of existing.slice(1))
|
|
208
255
|
await this.deleteComment(duplicate);
|
|
209
256
|
}
|