patchrelay 0.7.7 → 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.7",
4
- "commit": "b39ec3de1f37",
5
- "builtAt": "2026-03-17T09:39:30.228Z"
3
+ "version": "0.7.9",
4
+ "commit": "9f2abbe93f17",
5
+ "builtAt": "2026-03-17T10:42:10.173Z"
6
6
  }
package/dist/config.js CHANGED
@@ -54,7 +54,6 @@ const configSchema = z.object({
54
54
  level: z.enum(["debug", "info", "warn", "error"]).default("info"),
55
55
  format: z.literal("logfmt").default("logfmt"),
56
56
  file_path: z.string().min(1).default(getDefaultLogPath()),
57
- webhook_archive_dir: z.string().optional(),
58
57
  }),
59
58
  database: z.object({
60
59
  path: z.string().min(1).default(getDefaultDatabasePath()),
@@ -314,7 +313,6 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
314
313
  throw new Error(`Missing env var ${parsed.linear.token_encryption_key_env}`);
315
314
  }
316
315
  const logFilePath = env.PATCHRELAY_LOG_FILE ?? parsed.logging.file_path;
317
- const webhookArchiveDir = env.PATCHRELAY_WEBHOOK_ARCHIVE_DIR ?? parsed.logging.webhook_archive_dir;
318
316
  const oauthRedirectUri = parsed.linear.oauth.redirect_uri ?? deriveLinearOAuthRedirectUri(parsed.server);
319
317
  const config = {
320
318
  server: {
@@ -333,7 +331,6 @@ export function loadConfig(configPath = process.env.PATCHRELAY_CONFIG ?? getDefa
333
331
  level: env.PATCHRELAY_LOG_LEVEL ?? parsed.logging.level,
334
332
  format: parsed.logging.format,
335
333
  filePath: ensureAbsolutePath(logFilePath),
336
- ...(webhookArchiveDir ? { webhookArchiveDir: ensureAbsolutePath(webhookArchiveDir) } : {}),
337
334
  },
338
335
  database: {
339
336
  path: ensureAbsolutePath(env.PATCHRELAY_DB_PATH ?? parsed.database.path),
package/dist/index.js CHANGED
@@ -24,9 +24,6 @@ async function main() {
24
24
  await enforceServiceEnvPermissions(getAdjacentEnvFilePaths(configPath).serviceEnvPath);
25
25
  await ensureDir(dirname(config.database.path));
26
26
  await ensureDir(dirname(config.logging.filePath));
27
- if (config.logging.webhookArchiveDir) {
28
- await ensureDir(config.logging.webhookArchiveDir);
29
- }
30
27
  for (const project of config.projects) {
31
28
  await ensureDir(project.worktreeRoot);
32
29
  }
@@ -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,
package/dist/preflight.js CHANGED
@@ -66,12 +66,6 @@ export async function runPreflight(config) {
66
66
  checks.push(...checkPath("database", path.dirname(config.database.path), "directory", { createIfMissing: true, writable: true }));
67
67
  checks.push(checkDatabaseSchema(config));
68
68
  checks.push(...checkPath("logging", path.dirname(config.logging.filePath), "directory", { createIfMissing: true, writable: true }));
69
- if (config.logging.webhookArchiveDir) {
70
- checks.push(...checkPath("archive", config.logging.webhookArchiveDir, "directory", { createIfMissing: true, writable: true }));
71
- }
72
- else {
73
- checks.push(warn("archive", "Raw webhook archival is disabled"));
74
- }
75
69
  if (config.projects.length === 0) {
76
70
  checks.push(warn("projects", "No projects are configured yet; add one with `patchrelay project apply <id> <repo-path>` before connecting Linear"));
77
71
  }
@@ -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":
@@ -1,4 +1,3 @@
1
- import { archiveWebhook } from "./webhook-archive.js";
2
1
  import { normalizeWebhook } from "./webhooks.js";
3
2
  import { redactSensitiveHeaders, timestampMsWithinSkew, verifyHmacSha256Hex } from "./utils.js";
4
3
  export async function acceptIncomingWebhook(params) {
@@ -35,15 +34,6 @@ export async function acceptIncomingWebhook(params) {
35
34
  const headersJson = JSON.stringify(sanitizedHeaders);
36
35
  const payloadJson = JSON.stringify(payload);
37
36
  logWebhookSummary(params.logger, normalized);
38
- await archiveAcceptedPayload({
39
- config: params.config,
40
- logger: params.logger,
41
- webhookId: params.webhookId,
42
- receivedAt,
43
- headers: params.headers,
44
- rawBody: params.rawBody,
45
- payload,
46
- });
47
37
  const stored = params.stores.webhookEvents.insertWebhookEvent({
48
38
  webhookId: params.webhookId,
49
39
  receivedAt,
@@ -127,22 +117,3 @@ function logWebhookSummary(logger, normalized) {
127
117
  issueId: normalized.issue?.id,
128
118
  }, "Webhook metadata");
129
119
  }
130
- async function archiveAcceptedPayload(params) {
131
- if (!params.config.logging.webhookArchiveDir) {
132
- return;
133
- }
134
- try {
135
- const archivePath = await archiveWebhook({
136
- archiveDir: params.config.logging.webhookArchiveDir,
137
- webhookId: params.webhookId,
138
- receivedAt: params.receivedAt,
139
- headers: redactSensitiveHeaders(params.headers),
140
- rawBody: params.rawBody,
141
- payload: params.payload,
142
- });
143
- params.logger.debug({ webhookId: params.webhookId, archivePath }, "Archived webhook to local file");
144
- }
145
- catch (error) {
146
- params.logger.error({ webhookId: params.webhookId, error }, "Failed to archive webhook to local file");
147
- }
148
- }
@@ -8,7 +8,7 @@ export class StageAgentActivityPublisher {
8
8
  this.linearProvider = linearProvider;
9
9
  this.logger = logger;
10
10
  }
11
- async publishForSession(projectId, agentSessionId, content) {
11
+ async publishForSession(projectId, agentSessionId, content, options) {
12
12
  const linear = await this.linearProvider.forProject(projectId);
13
13
  if (!linear) {
14
14
  return;
@@ -17,7 +17,7 @@ export class StageAgentActivityPublisher {
17
17
  await linear.createAgentActivity({
18
18
  agentSessionId,
19
19
  content,
20
- ephemeral: content.type === "thought" || content.type === "action",
20
+ ephemeral: options?.ephemeral ?? (content.type === "thought" || content.type === "action"),
21
21
  });
22
22
  }
23
23
  catch (error) {
@@ -27,11 +27,11 @@ export class StageAgentActivityPublisher {
27
27
  }, "Failed to publish Linear agent activity");
28
28
  }
29
29
  }
30
- async publishForIssue(issue, content) {
30
+ async publishForIssue(issue, content, options) {
31
31
  if (!issue.activeAgentSessionId) {
32
32
  return;
33
33
  }
34
- await this.publishForSession(issue.projectId, issue.activeAgentSessionId, content);
34
+ await this.publishForSession(issue.projectId, issue.activeAgentSessionId, content, options);
35
35
  }
36
36
  async updateSession(params) {
37
37
  const linear = await this.linearProvider.forProject(params.projectId);
@@ -84,12 +84,9 @@ export class StageLifecyclePublisher {
84
84
  await linear.createAgentActivity({
85
85
  agentSessionId: issue.activeAgentSessionId,
86
86
  content: {
87
- type: "action",
88
- action: "running_workflow",
89
- parameter: stage,
90
- result: `PatchRelay started the ${stage} workflow.`,
87
+ type: "thought",
88
+ body: `PatchRelay started the ${stage} workflow and is working in the background.`,
91
89
  },
92
- ephemeral: true,
93
90
  });
94
91
  return true;
95
92
  }
@@ -66,9 +66,9 @@ export class AgentSessionWebhookHandler {
66
66
  if (desiredStage) {
67
67
  await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildPreparingSessionPlan(desiredStage)));
68
68
  await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
69
- type: "response",
70
- body: `PatchRelay picked this up and is preparing the ${desiredStage} workflow.`,
71
- });
69
+ type: "thought",
70
+ body: `PatchRelay started working on the ${desiredStage} workflow and is preparing the workspace.`,
71
+ }, { ephemeral: false });
72
72
  return;
73
73
  }
74
74
  if (activeStage) {
@@ -137,9 +137,9 @@ export class AgentSessionWebhookHandler {
137
137
  if (!activeRunLease && desiredStage) {
138
138
  await this.agentActivity.updateSession(buildSessionUpdateParams(project.id, normalized.agentSession.id, issue?.issueKey ?? normalized.issue?.identifier, buildPreparingSessionPlan(desiredStage)));
139
139
  await this.agentActivity.publishForSession(project.id, normalized.agentSession.id, {
140
- type: "response",
141
- body: `PatchRelay received your prompt and is preparing the ${desiredStage} workflow.`,
142
- });
140
+ type: "thought",
141
+ body: `PatchRelay is preparing the ${desiredStage} workflow from your latest prompt.`,
142
+ }, { ephemeral: false });
143
143
  return;
144
144
  }
145
145
  if (!activeRunLease && !desiredStage && (promptBody || promptContext)) {
package/dist/webhooks.js CHANGED
@@ -1,9 +1,55 @@
1
+ function getPayloadRecord(payload) {
2
+ return payload;
3
+ }
4
+ function getPayloadData(payload) {
5
+ return asRecord(getPayloadRecord(payload).data) ?? getPayloadRecord(payload);
6
+ }
7
+ function getNestedRecord(record, path) {
8
+ let current = record;
9
+ for (const segment of path) {
10
+ const currentRecord = asRecord(current);
11
+ if (!currentRecord) {
12
+ return undefined;
13
+ }
14
+ current = currentRecord[segment];
15
+ }
16
+ return asRecord(current);
17
+ }
18
+ function getFirstNestedRecord(record, paths) {
19
+ for (const path of paths) {
20
+ const candidate = getNestedRecord(record, path);
21
+ if (candidate) {
22
+ return candidate;
23
+ }
24
+ }
25
+ return undefined;
26
+ }
27
+ function looksLikeIssueRecord(record) {
28
+ if (!record) {
29
+ return false;
30
+ }
31
+ return Boolean(getString(record, "identifier") ||
32
+ getString(record, "title") ||
33
+ getString(record, "delegateId") ||
34
+ asRecord(record.delegate) ||
35
+ asRecord(record.team) ||
36
+ asRecord(record.state) ||
37
+ Array.isArray(record.labels));
38
+ }
1
39
  function deriveTriggerEvent(payload) {
2
- if (payload.type === "AgentSessionEvent") {
3
- if (payload.action === "created") {
40
+ const data = getPayloadData(payload);
41
+ const hasAgentSession = Boolean(getFirstNestedRecord(data, [
42
+ ["agentSession"],
43
+ ["session"],
44
+ ["agentSessionEvent", "agentSession"],
45
+ ["payload", "agentSession"],
46
+ ["resource", "agentSession"],
47
+ ])) || Boolean(getString(data, "agentSessionId"));
48
+ if (payload.type === "AgentSessionEvent" || payload.type === "AgentSession" || hasAgentSession) {
49
+ if (payload.action === "created" || payload.action === "create") {
4
50
  return "agentSessionCreated";
5
51
  }
6
- if (payload.action === "prompted") {
52
+ if (payload.action === "prompted" || payload.action === "prompt") {
7
53
  return "agentPrompted";
8
54
  }
9
55
  return "issueUpdated";
@@ -93,28 +139,42 @@ function extractLabelNames(record) {
93
139
  return [];
94
140
  }
95
141
  function extractIssueMetadata(payload) {
96
- const data = asRecord(payload.data);
97
- if (!data) {
98
- return undefined;
99
- }
142
+ const data = getPayloadData(payload);
143
+ const sessionRecord = getFirstNestedRecord(data, [
144
+ ["agentSession"],
145
+ ["session"],
146
+ ["agentSessionEvent", "agentSession"],
147
+ ["payload", "agentSession"],
148
+ ["resource", "agentSession"],
149
+ ]) ?? data;
150
+ const commentRecord = getFirstNestedRecord(data, [["comment"]]);
151
+ const notificationRecord = getFirstNestedRecord(data, [["notification"]]) ?? data;
100
152
  const issueRecord = payload.type === "Issue"
101
153
  ? data
102
- : payload.type === "AgentSessionEvent"
103
- ? (() => {
104
- const sessionRecord = asRecord(data.agentSession) ?? data;
105
- return asRecord(sessionRecord.issue) ?? asRecord(data.issue) ?? sessionRecord;
106
- })()
107
- : payload.type === "AppUserNotification"
108
- ? (() => {
109
- const notificationRecord = asRecord(data.notification) ?? data;
110
- return (asRecord(notificationRecord.issue) ??
111
- asRecord(asRecord(notificationRecord.comment)?.issue) ??
112
- asRecord(data.issue));
113
- })()
114
- : (() => {
115
- const nestedIssue = asRecord(data.issue);
116
- return nestedIssue ?? data;
117
- })();
154
+ : payload.type === "AppUserNotification"
155
+ ? getFirstNestedRecord(notificationRecord, [["issue"], ["comment", "issue"]]) ?? getFirstNestedRecord(data, [["issue"]])
156
+ : getFirstNestedRecord(data, [
157
+ ["issue"],
158
+ ["agentSession", "issue"],
159
+ ["session", "issue"],
160
+ ["agentSessionEvent", "issue"],
161
+ ["agentSessionEvent", "agentSession", "issue"],
162
+ ["payload", "issue"],
163
+ ["payload", "agentSession", "issue"],
164
+ ["resource", "issue"],
165
+ ["resource", "agentSession", "issue"],
166
+ ["comment", "issue"],
167
+ ["comment", "parent", "issue"],
168
+ ["comment", "commentThread", "issue"],
169
+ ["comment", "parentEntity", "issue"],
170
+ ["parent", "issue"],
171
+ ["commentThread", "issue"],
172
+ ["parentEntity", "issue"],
173
+ ["notification", "issue"],
174
+ ["notification", "comment", "issue"],
175
+ ]) ??
176
+ (looksLikeIssueRecord(sessionRecord) ? sessionRecord : undefined) ??
177
+ (looksLikeIssueRecord(commentRecord) ? commentRecord : undefined);
118
178
  if (!issueRecord) {
119
179
  return undefined;
120
180
  }
@@ -184,16 +244,24 @@ function extractActorMetadata(payload) {
184
244
  return fallbacks.find(Boolean);
185
245
  }
186
246
  function extractCommentMetadata(payload) {
187
- if (payload.type !== "Comment") {
188
- return undefined;
189
- }
190
- const data = asRecord(payload.data);
191
- if (!data) {
247
+ const data = getPayloadData(payload);
248
+ const commentRecord = payload.type === "Comment"
249
+ ? data
250
+ : getFirstNestedRecord(data, [
251
+ ["comment"],
252
+ ["agentSession", "comment"],
253
+ ["session", "comment"],
254
+ ["agentSessionEvent", "comment"],
255
+ ["payload", "comment"],
256
+ ["resource", "comment"],
257
+ ["notification", "comment"],
258
+ ]);
259
+ if (!commentRecord) {
192
260
  return undefined;
193
261
  }
194
- const id = getString(data, "id");
195
- const body = getString(data, "body");
196
- const userRecord = asRecord(data.user);
262
+ const id = getString(commentRecord, "id");
263
+ const body = getString(commentRecord, "body");
264
+ const userRecord = asRecord(commentRecord.user);
197
265
  const userName = getString(userRecord ?? {}, "name");
198
266
  if (!id) {
199
267
  return undefined;
@@ -205,23 +273,43 @@ function extractCommentMetadata(payload) {
205
273
  };
206
274
  }
207
275
  function extractAgentSessionMetadata(payload) {
208
- if (payload.type !== "AgentSessionEvent") {
276
+ const data = getPayloadData(payload);
277
+ const sessionRecord = getFirstNestedRecord(data, [
278
+ ["agentSession"],
279
+ ["session"],
280
+ ["agentSessionEvent", "agentSession"],
281
+ ["payload", "agentSession"],
282
+ ["resource", "agentSession"],
283
+ ]) ?? (payload.type === "AgentSession" ? data : undefined);
284
+ if (payload.type !== "AgentSessionEvent" && payload.type !== "AgentSession" && !sessionRecord && !getString(data, "agentSessionId")) {
209
285
  return undefined;
210
286
  }
211
- const data = asRecord(payload.data);
212
- if (!data) {
213
- return undefined;
214
- }
215
- const sessionRecord = asRecord(data.agentSession) ?? data;
216
- const id = getString(sessionRecord, "id") ?? getString(data, "agentSessionId");
287
+ const id = getString(sessionRecord ?? {}, "id") ?? getString(data, "agentSessionId");
217
288
  if (!id) {
218
289
  return undefined;
219
290
  }
220
- const agentActivity = asRecord(data.agentActivity);
221
- const commentRecord = asRecord(data.comment) ?? asRecord(sessionRecord.comment);
222
- const promptContext = getString(data, "promptContext") ?? getString(sessionRecord, "promptContext");
223
- const promptBody = getString(agentActivity ?? {}, "body") ?? getString(commentRecord ?? {}, "body");
224
- const issueCommentId = getString(commentRecord ?? {}, "id");
291
+ const agentActivity = getFirstNestedRecord(data, [
292
+ ["agentActivity"],
293
+ ["agentSession", "agentActivity"],
294
+ ["session", "agentActivity"],
295
+ ["agentSessionEvent", "agentActivity"],
296
+ ["payload", "agentActivity"],
297
+ ["resource", "agentActivity"],
298
+ ]);
299
+ const commentRecord = getFirstNestedRecord(data, [
300
+ ["comment"],
301
+ ["agentSession", "comment"],
302
+ ["session", "comment"],
303
+ ["agentSessionEvent", "comment"],
304
+ ["payload", "comment"],
305
+ ["resource", "comment"],
306
+ ]) ??
307
+ getFirstNestedRecord(sessionRecord, [["comment"]]);
308
+ const promptContext = getString(data, "promptContext") ?? getString(sessionRecord ?? {}, "promptContext");
309
+ const promptBody = getString(agentActivity ?? {}, "body") ??
310
+ getString(commentRecord ?? {}, "body") ??
311
+ getString(data, "body");
312
+ const issueCommentId = getString(commentRecord ?? {}, "id") ?? getString(data, "issueCommentId");
225
313
  return {
226
314
  id,
227
315
  ...(promptContext ? { promptContext } : {}),
@@ -230,10 +318,7 @@ function extractAgentSessionMetadata(payload) {
230
318
  };
231
319
  }
232
320
  function extractInstallationMetadata(payload) {
233
- const data = asRecord(payload.data);
234
- if (!data) {
235
- return undefined;
236
- }
321
+ const data = getPayloadData(payload);
237
322
  if (payload.type === "PermissionChange") {
238
323
  const organizationId = getString(data, "organizationId");
239
324
  const oauthClientId = getString(data, "oauthClientId");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,24 +0,0 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- const REDACTED_HEADERS = new Set(["authorization", "cookie", "set-cookie", "linear-signature"]);
4
- function sanitizePathSegment(value) {
5
- return value.replace(/[^a-zA-Z0-9._-]+/g, "-");
6
- }
7
- export async function archiveWebhook(params) {
8
- const datePrefix = params.receivedAt.slice(0, 10);
9
- const directory = path.join(params.archiveDir, datePrefix);
10
- const fileName = `${params.receivedAt.replace(/[:.]/g, "-")}-${sanitizePathSegment(params.webhookId)}.json`;
11
- const filePath = path.join(directory, fileName);
12
- await mkdir(directory, { recursive: true });
13
- await writeFile(filePath, JSON.stringify({
14
- webhookId: params.webhookId,
15
- receivedAt: params.receivedAt,
16
- headers: redactHeaders(params.headers),
17
- rawBodyUtf8: params.rawBody.toString("utf8"),
18
- payload: params.payload,
19
- }, null, 2), "utf8");
20
- return filePath;
21
- }
22
- function redactHeaders(headers) {
23
- return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, REDACTED_HEADERS.has(key.toLowerCase()) ? "[redacted]" : value]));
24
- }