patchrelay 0.7.8 → 0.7.9

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.7.8",
4
- "commit": "ef8346869c35",
5
- "builtAt": "2026-03-17T10:30:51.043Z"
3
+ "version": "0.7.9",
4
+ "commit": "9f2abbe93f17",
5
+ "builtAt": "2026-03-17T10:42:10.173Z"
6
6
  }
@@ -15,6 +15,10 @@ export class LinearGraphqlClient {
15
15
  identifier
16
16
  title
17
17
  url
18
+ delegate {
19
+ id
20
+ name
21
+ }
18
22
  state {
19
23
  id
20
24
  name
@@ -65,6 +69,10 @@ export class LinearGraphqlClient {
65
69
  identifier
66
70
  title
67
71
  url
72
+ delegate {
73
+ id
74
+ name
75
+ }
68
76
  state {
69
77
  id
70
78
  name
@@ -290,6 +298,8 @@ export class LinearGraphqlClient {
290
298
  ...(issue.state?.name ? { stateName: issue.state.name } : {}),
291
299
  ...(issue.team?.id ? { teamId: issue.team.id } : {}),
292
300
  ...(issue.team?.key ? { teamKey: issue.team.key } : {}),
301
+ ...(issue.delegate?.id ? { delegateId: issue.delegate.id } : {}),
302
+ ...(issue.delegate?.name ? { delegateName: issue.delegate.name } : {}),
293
303
  workflowStates: (issue.team?.states?.nodes ?? []).map((state) => ({
294
304
  id: state.id,
295
305
  name: state.name,
@@ -10,6 +10,7 @@ import { normalizeWebhook } from "./webhooks.js";
10
10
  export class ServiceWebhookProcessor {
11
11
  config;
12
12
  stores;
13
+ linearProvider;
13
14
  enqueueIssue;
14
15
  logger;
15
16
  feed;
@@ -20,6 +21,7 @@ export class ServiceWebhookProcessor {
20
21
  constructor(config, stores, linearProvider, codex, enqueueIssue, logger, feed) {
21
22
  this.config = config;
22
23
  this.stores = stores;
24
+ this.linearProvider = linearProvider;
23
25
  this.enqueueIssue = enqueueIssue;
24
26
  this.logger = logger;
25
27
  this.feed = feed;
@@ -115,13 +117,15 @@ export class ServiceWebhookProcessor {
115
117
  }
116
118
  this.stores.webhookEvents.assignWebhookProject(webhookEventId, project.id);
117
119
  const receipt = this.ensureEventReceipt(event, project.id, normalized.issue.id);
118
- const issueState = this.desiredStageRecorder.record(project, normalized, receipt ? { eventReceiptId: receipt.id } : undefined);
119
- const observation = describeWebhookObservation(normalized, issueState.delegatedToPatchRelay);
120
+ const hydrated = await this.hydrateIssueContext(project.id, normalized);
121
+ const hydratedIssue = hydrated.issue ?? normalized.issue;
122
+ const issueState = this.desiredStageRecorder.record(project, hydrated, receipt ? { eventReceiptId: receipt.id } : undefined);
123
+ const observation = describeWebhookObservation(hydrated, issueState.delegatedToPatchRelay);
120
124
  if (observation) {
121
125
  this.feed?.publish({
122
126
  level: "info",
123
127
  kind: observation.kind,
124
- issueKey: normalized.issue.identifier,
128
+ issueKey: hydratedIssue.identifier,
125
129
  projectId: project.id,
126
130
  ...(observation.status ? { status: observation.status } : {}),
127
131
  summary: observation.summary,
@@ -129,45 +133,45 @@ export class ServiceWebhookProcessor {
129
133
  });
130
134
  }
131
135
  await this.agentSessionHandler.handle({
132
- normalized,
136
+ normalized: hydrated,
133
137
  project,
134
138
  issue: issueState.issue,
135
139
  desiredStage: issueState.desiredStage,
136
140
  delegatedToPatchRelay: issueState.delegatedToPatchRelay,
137
141
  });
138
- await this.commentHandler.handle(normalized, project);
142
+ await this.commentHandler.handle(hydrated, project);
139
143
  this.stores.webhookEvents.markWebhookProcessed(webhookEventId, "processed");
140
144
  this.markEventReceiptProcessed(event.webhookId, "processed");
141
145
  if (issueState.desiredStage) {
142
146
  this.feed?.publish({
143
147
  level: "info",
144
148
  kind: "stage",
145
- issueKey: normalized.issue.identifier,
149
+ issueKey: hydratedIssue.identifier,
146
150
  projectId: project.id,
147
151
  stage: issueState.desiredStage,
148
152
  status: "queued",
149
153
  summary: `Queued ${issueState.desiredStage} workflow`,
150
- detail: `Triggered by ${normalized.triggerEvent}${normalized.issue.stateName ? ` from ${normalized.issue.stateName}` : ""}.`,
154
+ detail: `Triggered by ${hydrated.triggerEvent}${hydratedIssue.stateName ? ` from ${hydratedIssue.stateName}` : ""}.`,
151
155
  });
152
156
  this.logger.info({
153
157
  webhookEventId,
154
158
  webhookId: event.webhookId,
155
159
  projectId: project.id,
156
- issueKey: normalized.issue.identifier,
157
- issueId: normalized.issue.id,
160
+ issueKey: hydratedIssue.identifier,
161
+ issueId: hydratedIssue.id,
158
162
  desiredStage: issueState.desiredStage,
159
163
  delegatedToPatchRelay: issueState.delegatedToPatchRelay,
160
164
  }, "Recorded desired stage from webhook and enqueued issue execution");
161
- this.enqueueIssue(project.id, normalized.issue.id);
165
+ this.enqueueIssue(project.id, hydratedIssue.id);
162
166
  return;
163
167
  }
164
168
  this.logger.info({
165
169
  webhookEventId,
166
170
  webhookId: event.webhookId,
167
171
  projectId: project.id,
168
- issueKey: normalized.issue.identifier,
169
- issueId: normalized.issue.id,
170
- triggerEvent: normalized.triggerEvent,
172
+ issueKey: hydratedIssue.identifier,
173
+ issueId: hydratedIssue.id,
174
+ triggerEvent: hydrated.triggerEvent,
171
175
  delegatedToPatchRelay: issueState.delegatedToPatchRelay,
172
176
  }, "Processed webhook without enqueuing a new stage run");
173
177
  }
@@ -194,6 +198,37 @@ export class ServiceWebhookProcessor {
194
198
  throw err;
195
199
  }
196
200
  }
201
+ async hydrateIssueContext(projectId, normalized) {
202
+ if (!normalized.issue) {
203
+ return normalized;
204
+ }
205
+ if (normalized.triggerEvent !== "agentSessionCreated" && normalized.triggerEvent !== "agentPrompted") {
206
+ return normalized;
207
+ }
208
+ if (hasCompleteIssueContext(normalized.issue)) {
209
+ return normalized;
210
+ }
211
+ const linear = await this.linearProvider.forProject(projectId);
212
+ if (!linear) {
213
+ return normalized;
214
+ }
215
+ try {
216
+ const liveIssue = await linear.getIssue(normalized.issue.id);
217
+ return {
218
+ ...normalized,
219
+ issue: mergeIssueMetadata(normalized.issue, liveIssue),
220
+ };
221
+ }
222
+ catch (error) {
223
+ this.logger.warn({
224
+ projectId,
225
+ issueId: normalized.issue.id,
226
+ triggerEvent: normalized.triggerEvent,
227
+ error: sanitizeDiagnosticText(error instanceof Error ? error.message : String(error)),
228
+ }, "Failed to hydrate sparse Linear issue context for agent session webhook");
229
+ return normalized;
230
+ }
231
+ }
197
232
  assignEventReceiptContext(webhookId, projectId, linearIssueId) {
198
233
  const receipt = this.lookupEventReceipt(webhookId);
199
234
  if (!receipt) {
@@ -234,6 +269,24 @@ export class ServiceWebhookProcessor {
234
269
  return this.stores.eventReceipts.getEventReceipt(inserted.id);
235
270
  }
236
271
  }
272
+ function hasCompleteIssueContext(issue) {
273
+ return Boolean(issue.stateName && issue.delegateId && issue.teamId && issue.teamKey);
274
+ }
275
+ function mergeIssueMetadata(issue, liveIssue) {
276
+ return {
277
+ ...issue,
278
+ ...(issue.identifier ? {} : liveIssue.identifier ? { identifier: liveIssue.identifier } : {}),
279
+ ...(issue.title ? {} : liveIssue.title ? { title: liveIssue.title } : {}),
280
+ ...(issue.url ? {} : liveIssue.url ? { url: liveIssue.url } : {}),
281
+ ...(issue.teamId ? {} : liveIssue.teamId ? { teamId: liveIssue.teamId } : {}),
282
+ ...(issue.teamKey ? {} : liveIssue.teamKey ? { teamKey: liveIssue.teamKey } : {}),
283
+ ...(issue.stateId ? {} : liveIssue.stateId ? { stateId: liveIssue.stateId } : {}),
284
+ ...(issue.stateName ? {} : liveIssue.stateName ? { stateName: liveIssue.stateName } : {}),
285
+ ...(issue.delegateId ? {} : liveIssue.delegateId ? { delegateId: liveIssue.delegateId } : {}),
286
+ ...(issue.delegateName ? {} : liveIssue.delegateName ? { delegateName: liveIssue.delegateName } : {}),
287
+ labelNames: issue.labelNames.length > 0 ? issue.labelNames : (liveIssue.labels ?? []).map((label) => label.name),
288
+ };
289
+ }
237
290
  function describeWebhookObservation(normalized, delegatedToPatchRelay) {
238
291
  switch (normalized.triggerEvent) {
239
292
  case "delegateChanged":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {