@mastra/github-signals 0.0.0 → 0.1.1-alpha.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # @mastra/github-signals
2
+
3
+ ## 0.1.1-alpha.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Gate GitHub signal notifications behind author permission checks to guard against prompt injection from random commenters. Only comments from users with write access (admin, maintain, write) trigger notifications. Bot comments are opt-in via an allowlist that defaults to CodeRabbit and Devin, with `ignoredBots` still available as an explicit blocklist. Unauthorized latest comments are excluded before notification classification so noisy bot edits do not render in notification metadata or mask the latest authorized comment. Scheduled polls now include comments and detect latest-comment timestamp changes so comment notifications are not lost behind stale or unchanged thread hashes. Comment activity notifications render the latest authorized comment author and excerpt as high-priority GitHub signal updates. ([#17590](https://github.com/mastra-ai/mastra/pull/17590))
8
+
9
+ New options on `GithubSignalsOptions`:
10
+ - `authorizedPermissions` — permission levels that authorize human commenters (default: `['admin', 'maintain', 'write']`)
11
+ - `authorizedBots` — bot logins authorized to trigger notifications (default: `['coderabbitai[bot]', 'devin-ai-integration[bot]']`)
12
+ - `ignoredBots` — bot logins whose comments should NOT trigger notifications, even if authorized
13
+ - `permissionResolver` — injectable resolver for looking up collaborator permissions (default: `gh api`)
14
+
15
+ - Updated dependencies [[`2bccba4`](https://github.com/mastra-ai/mastra/commit/2bccba4c03cadc815c2d54cbf4dd43a922140a8d), [`2bccba4`](https://github.com/mastra-ai/mastra/commit/2bccba4c03cadc815c2d54cbf4dd43a922140a8d), [`f2ab060`](https://github.com/mastra-ai/mastra/commit/f2ab060162bea81505fda553e2cee29c1979fd04), [`5d302c8`](https://github.com/mastra-ai/mastra/commit/5d302c8eda1a6ac74eab5e442c4f64db6cc97a06)]:
16
+ - @mastra/core@1.42.0-alpha.1
package/dist/index.cjs CHANGED
@@ -26,6 +26,10 @@ var GITHUB_SUBSCRIBE_PR_TAG = "github-subscribe-pr";
26
26
  var GITHUB_UNSUBSCRIBE_PR_TAG = "github-unsubscribe-pr";
27
27
  var GITHUB_SYNC_STATUS_TAG = "github-sync-status";
28
28
  var GITHUB_SIGNALS_METADATA_KEY = "githubSignals";
29
+ var DEFAULT_AUTHORIZED_PERMISSIONS = ["admin", "maintain", "write"];
30
+ var DEFAULT_AUTHORIZED_BOTS = ["coderabbitai[bot]", "devin-ai-integration[bot]"];
31
+ var PERMISSION_CACHE_TTL_MS = 5 * 60 * 1e3;
32
+ var AUTHOR_GATED_NOTIFICATION_KINDS = /* @__PURE__ */ new Set(["pull-request-activity", "pull-request-review-activity"]);
29
33
  var createGithubTool = tools.createTool;
30
34
  function isPlainObject(value) {
31
35
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -146,6 +150,36 @@ function getPrLabel(subscription, snapshot) {
146
150
  function getMergedNotificationSummary(label) {
147
151
  return `${label} was merged. This thread has been automatically unsubscribed from this PR. Resubscribe if you still need updates.`;
148
152
  }
153
+ function getCommentExcerpt(body) {
154
+ const excerpt = body.replace(/\s+/g, " ").trim();
155
+ return excerpt.length > 240 ? `${excerpt.slice(0, 237)}...` : excerpt;
156
+ }
157
+ function getCommentNotificationSummary(pr, snapshot) {
158
+ if (!snapshot.latestCommentAuthor || !snapshot.latestCommentBody) return void 0;
159
+ return `${snapshot.latestCommentAuthor} commented on ${pr}: ${getCommentExcerpt(snapshot.latestCommentBody)}`;
160
+ }
161
+ var githubActivityNotificationPriority = {
162
+ high: 0,
163
+ medium: 1
164
+ };
165
+ function getGithubActivityNotificationRank(notification) {
166
+ return notification.kind === "pull-request-activity" ? 0 : 1;
167
+ }
168
+ function compareGithubActivityNotifications(a, b) {
169
+ if (!a && !b) return 0;
170
+ if (!a) return 1;
171
+ if (!b) return -1;
172
+ const priorityComparison = githubActivityNotificationPriority[a.priority] - githubActivityNotificationPriority[b.priority];
173
+ if (priorityComparison !== 0) return priorityComparison;
174
+ return getGithubActivityNotificationRank(a) - getGithubActivityNotificationRank(b);
175
+ }
176
+ function classifyGithubCommentActivityNotification(input) {
177
+ if (isBotOnlyActivity(input.snapshot)) return void 0;
178
+ const pr = `${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}`;
179
+ const summary = getCommentNotificationSummary(pr, input.snapshot);
180
+ if (!summary) return void 0;
181
+ return { kind: "pull-request-activity", priority: "high", summary };
182
+ }
149
183
  function getCheckUpdatedTime(check) {
150
184
  const value = check.updatedAt ? Date.parse(check.updatedAt) : Number.NaN;
151
185
  return Number.isFinite(value) ? value : 0;
@@ -269,10 +303,11 @@ function classifyGithubActivityNotification(input) {
269
303
  }
270
304
  if (input.snapshot.ciState === "pending" && input.subscription.lastObservedCiState === "pending") return void 0;
271
305
  if (isBotOnlyActivity(input.snapshot)) return void 0;
306
+ const commentSummary = getCommentNotificationSummary(pr, input.snapshot);
272
307
  return {
273
308
  kind: "pull-request-activity",
274
- priority: "medium",
275
- summary: `${pr} has new activity${input.snapshot.title ? `: ${input.snapshot.title}` : ""}`
309
+ priority: commentSummary ? "high" : "medium",
310
+ summary: commentSummary ?? `${pr} has new activity${input.snapshot.title ? `: ${input.snapshot.title}` : ""}`
276
311
  };
277
312
  }
278
313
  function classifyGithubBaselineNotification(input) {
@@ -397,13 +432,15 @@ var GitcrawlSyncClient = class {
397
432
  join threads t on t.id=rt.thread_id
398
433
  join repositories r on r.id=t.repo_id
399
434
  where r.owner=${owner} and r.name=${repo} and t.number=${number} and rt.is_resolved=0`);
400
- const [latestComment] = await queryGitcrawlDb(`select c.author_login, c.author_type, c.is_bot
435
+ const latestComments = await queryGitcrawlDb(`select c.author_login, c.author_type, c.is_bot, c.body, json_extract(c.raw_json, '$.html_url') as html_url,
436
+ coalesce(c.updated_at_gh, c.created_at_gh) as updated_at
401
437
  from comments c
402
438
  join threads t on t.id=c.thread_id
403
439
  join repositories r on r.id=t.repo_id
404
440
  where r.owner=${owner} and r.name=${repo} and t.number=${number}
405
441
  order by coalesce(c.updated_at_gh, c.created_at_gh) desc
406
- limit 1`);
442
+ limit 20`);
443
+ const latestComment = latestComments[0];
407
444
  const checks = normalizeGithubChecksForSnapshot({
408
445
  checkRows: checkRows.map((row) => ({
409
446
  source: "check",
@@ -465,7 +502,18 @@ var GitcrawlSyncClient = class {
465
502
  latestReviewThreadAt: readString(reviewState?.latest_review_thread_at),
466
503
  latestCommentAuthor: readString(latestComment?.author_login),
467
504
  latestCommentAuthorType: readString(latestComment?.author_type),
468
- latestCommentIsBot: latestComment?.is_bot === 1
505
+ latestCommentIsBot: latestComment?.is_bot === 1,
506
+ latestCommentBody: readString(latestComment?.body),
507
+ latestCommentUrl: readString(latestComment?.html_url),
508
+ latestCommentUpdatedAt: readString(latestComment?.updated_at),
509
+ latestComments: latestComments.map((comment) => ({
510
+ author: readString(comment.author_login),
511
+ authorType: readString(comment.author_type),
512
+ isBot: comment.is_bot === 1,
513
+ body: readString(comment.body),
514
+ url: readString(comment.html_url),
515
+ updatedAt: readString(comment.updated_at)
516
+ }))
469
517
  };
470
518
  } catch {
471
519
  return void 0;
@@ -520,9 +568,11 @@ var GithubSignals = class extends signals.SignalProvider {
520
568
  #syncClient;
521
569
  #repositoryResolver;
522
570
  #polling = /* @__PURE__ */ new Map();
571
+ #permissionCache = /* @__PURE__ */ new Map();
523
572
  #agent;
524
573
  #agentOptions = {};
525
574
  #subscriptionsChangedHandler;
575
+ #pollingChangedHandler;
526
576
  constructor(options = {}) {
527
577
  super();
528
578
  this.#options = options;
@@ -557,6 +607,9 @@ var GithubSignals = class extends signals.SignalProvider {
557
607
  onSubscriptionsChanged(handler) {
558
608
  this.#subscriptionsChangedHandler = handler;
559
609
  }
610
+ onPollingChanged(handler) {
611
+ this.#pollingChangedHandler = handler;
612
+ }
560
613
  __registerMastra(mastra) {
561
614
  super.__registerMastra(mastra);
562
615
  this.#ghMastra = mastra;
@@ -595,15 +648,13 @@ var GithubSignals = class extends signals.SignalProvider {
595
648
  this.#polling.delete(pollingKey);
596
649
  }
597
650
  if (this.#polling.has(key)) return true;
598
- let scheduledPollCount = options.pollImmediately ? 1 : 0;
599
651
  const runPoll = (pollOptions = {}) => {
600
652
  void this.#pollThread(input, pollOptions).catch((error) => {
601
653
  console.warn("GitHub PR polling failed:", error);
602
654
  });
603
655
  };
604
656
  const timer = setInterval(() => {
605
- scheduledPollCount += 1;
606
- runPoll({ includeComments: scheduledPollCount % 2 === 1 });
657
+ runPoll({ includeComments: true });
607
658
  }, this.#options.pollIntervalMs ?? 3e5);
608
659
  if (options.pollImmediately) runPoll({ includeComments: true });
609
660
  timer.unref?.();
@@ -620,6 +671,9 @@ var GithubSignals = class extends signals.SignalProvider {
620
671
  isPollingThread(input) {
621
672
  return this.#polling.has(this.#pollingKey(input));
622
673
  }
674
+ isPollingThreadRunning(input) {
675
+ return this.#polling.get(this.#pollingKey(input))?.running ?? false;
676
+ }
623
677
  getPollIntervalMs() {
624
678
  return this.#options.pollIntervalMs ?? 3e5;
625
679
  }
@@ -805,6 +859,9 @@ var GithubSignals = class extends signals.SignalProvider {
805
859
  #notifySubscriptionsChanged(input) {
806
860
  this.#subscriptionsChangedHandler?.(input);
807
861
  }
862
+ #notifyPollingChanged(input) {
863
+ this.#pollingChangedHandler?.(input);
864
+ }
808
865
  async #pollThread(input, options = {}) {
809
866
  const key = this.#pollingKey(input);
810
867
  const state = this.#polling.get(key);
@@ -812,6 +869,7 @@ var GithubSignals = class extends signals.SignalProvider {
812
869
  return 0;
813
870
  }
814
871
  if (state) state.running = true;
872
+ this.#notifyPollingChanged({ threadId: input.threadId, resourceId: input.resourceId, running: true });
815
873
  try {
816
874
  const { threadStore, loadedThread } = await this.#loadThread(input);
817
875
  const githubMetadata = getGithubMetadata(loadedThread.metadata);
@@ -830,7 +888,9 @@ var GithubSignals = class extends signals.SignalProvider {
830
888
  includeComments: options.includeComments
831
889
  };
832
890
  const syncResult = await this.#syncClient.syncPullRequest(syncInput);
833
- const snapshot = syncResult.ok ? await this.#syncClient.getPullRequestSnapshot?.(syncInput) : void 0;
891
+ let snapshot = syncResult.ok ? await this.#syncClient.getPullRequestSnapshot?.(syncInput) : void 0;
892
+ if (snapshot)
893
+ snapshot = await this.#filterUnauthorizedLatestComment(subscription.owner, subscription.repo, snapshot);
834
894
  const nextSubscription = {
835
895
  ...subscription,
836
896
  updatedAt: now,
@@ -843,25 +903,28 @@ var GithubSignals = class extends signals.SignalProvider {
843
903
  const previousContentHash = subscription.lastObservedContentHash;
844
904
  const previousThreadContentHash = subscription.lastObservedThreadContentHash;
845
905
  const previousHeadSha = subscription.lastObservedHeadSha;
906
+ const latestCommentChanged = !!previousGithubUpdatedAt && !!snapshot?.latestCommentUpdatedAt && Date.parse(snapshot.latestCommentUpdatedAt) > Date.parse(previousGithubUpdatedAt);
846
907
  if (snapshot) applySnapshotCursor(nextSubscription, snapshot);
847
908
  const isFirstObservation = syncResult.ok && snapshot && !previousGithubUpdatedAt && !previousContentHash;
848
909
  const legacyAggregateChanged = previousContentHash && snapshot?.contentHash && previousContentHash !== snapshot.contentHash && !previousThreadContentHash && !previousHeadSha;
849
- const changed = isFirstObservation || syncResult.ok && snapshot && (legacyAggregateChanged || previousThreadContentHash && snapshot.threadContentHash && previousThreadContentHash !== snapshot.threadContentHash || previousHeadSha && snapshot.headSha && previousHeadSha !== snapshot.headSha || subscription.lastObservedState && snapshot.state && subscription.lastObservedState !== snapshot.state || subscription.lastObservedMergeableState && snapshot.mergeableState && subscription.lastObservedMergeableState !== snapshot.mergeableState || subscription.lastObservedCiState && snapshot.ciState && subscription.lastObservedCiState !== snapshot.ciState || subscription.lastObservedReviewStateHash && snapshot.reviewStateHash && subscription.lastObservedReviewStateHash !== snapshot.reviewStateHash);
910
+ const changed = isFirstObservation || syncResult.ok && snapshot && (legacyAggregateChanged || latestCommentChanged || previousThreadContentHash && snapshot.threadContentHash && previousThreadContentHash !== snapshot.threadContentHash || previousHeadSha && snapshot.headSha && previousHeadSha !== snapshot.headSha || subscription.lastObservedState && snapshot.state && subscription.lastObservedState !== snapshot.state || subscription.lastObservedMergeableState && snapshot.mergeableState && subscription.lastObservedMergeableState !== snapshot.mergeableState || subscription.lastObservedCiState && snapshot.ciState && subscription.lastObservedCiState !== snapshot.ciState || subscription.lastObservedReviewStateHash && snapshot.reviewStateHash && subscription.lastObservedReviewStateHash !== snapshot.reviewStateHash);
850
911
  let shouldKeepSubscription = true;
851
- if (changed) {
852
- const notification = await this.#sendActivityNotification({
912
+ if (changed && snapshot) {
913
+ const notifications = await this.#sendActivityNotifications({
853
914
  polling: input,
854
915
  subscription,
855
916
  snapshot,
856
917
  previousGithubUpdatedAt,
857
- previousContentHash
918
+ previousContentHash,
919
+ latestCommentChanged
858
920
  });
859
- if (notification) {
921
+ const primaryNotification = notifications[0];
922
+ if (primaryNotification) {
860
923
  nextSubscription.lastNotificationAt = now;
861
- nextSubscription.lastNotificationKind = notification.kind;
862
- nextSubscription.lastNotificationPriority = notification.priority;
863
- nextSubscription.lastNotificationSummary = notification.summary;
864
- shouldKeepSubscription = notification.kind !== "pull-request-merged";
924
+ nextSubscription.lastNotificationKind = primaryNotification.kind;
925
+ nextSubscription.lastNotificationPriority = primaryNotification.priority;
926
+ nextSubscription.lastNotificationSummary = primaryNotification.summary;
927
+ shouldKeepSubscription = notifications.every((notification) => notification.kind !== "pull-request-merged");
865
928
  }
866
929
  }
867
930
  if (shouldKeepSubscription) subscriptions.push(nextSubscription);
@@ -884,17 +947,20 @@ var GithubSignals = class extends signals.SignalProvider {
884
947
  } finally {
885
948
  const latestState = this.#polling.get(key);
886
949
  if (latestState) latestState.running = false;
950
+ this.#notifyPollingChanged({ threadId: input.threadId, resourceId: input.resourceId, running: false });
887
951
  }
888
952
  }
889
- async #sendGithubNotification(input) {
953
+ #createGithubNotificationInput(input) {
890
954
  const failingChecks = getFailingChecks(input.snapshot);
891
955
  const pendingChecks = getPendingChecks(input.snapshot);
956
+ const latestCommentExcerpt = input.snapshot.latestCommentBody ? getCommentExcerpt(input.snapshot.latestCommentBody) : void 0;
957
+ const latestCommentDedupeSuffix = input.notification.kind === "pull-request-activity" && input.snapshot.latestCommentUrl ? `comment:${input.snapshot.latestCommentUrl}:${input.snapshot.latestCommentUpdatedAt ?? ""}` : input.dedupeSuffix;
892
958
  const notificationInput = {
893
959
  source: "github",
894
960
  kind: input.notification.kind,
895
961
  priority: input.notification.priority,
896
962
  summary: input.notification.summary,
897
- dedupeKey: `github:${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}:${input.dedupeSuffix}`,
963
+ dedupeKey: `github:${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}:${latestCommentDedupeSuffix}`,
898
964
  coalesceKey: `github:${input.subscription.owner}/${input.subscription.repo}#${input.subscription.number}:${input.notification.kind}`,
899
965
  attributes: {
900
966
  owner: input.subscription.owner,
@@ -908,6 +974,10 @@ var GithubSignals = class extends signals.SignalProvider {
908
974
  ...input.snapshot.mergeableState ? { mergeableState: input.snapshot.mergeableState } : {},
909
975
  ...input.snapshot.ciState ? { ciState: input.snapshot.ciState } : {},
910
976
  ...input.snapshot.unresolvedReviewThreads !== void 0 ? { unresolvedReviewThreads: input.snapshot.unresolvedReviewThreads } : {},
977
+ ...input.snapshot.latestCommentAuthor ? { latestCommentAuthor: input.snapshot.latestCommentAuthor } : {},
978
+ ...latestCommentExcerpt ? { latestCommentExcerpt } : {},
979
+ ...input.snapshot.latestCommentUrl ? { latestCommentUrl: input.snapshot.latestCommentUrl } : {},
980
+ ...input.snapshot.latestCommentUpdatedAt ? { latestCommentUpdatedAt: input.snapshot.latestCommentUpdatedAt } : {},
911
981
  ...failingChecks.length > 0 ? { failingChecks: failingChecks.map((check) => check.name).join(", ") } : {},
912
982
  ...pendingChecks.length > 0 ? { pendingChecks: pendingChecks.map((check) => check.name).join(", ") } : {}
913
983
  },
@@ -936,11 +1006,19 @@ var GithubSignals = class extends signals.SignalProvider {
936
1006
  latestCommentAuthor: input.snapshot.latestCommentAuthor,
937
1007
  latestCommentAuthorType: input.snapshot.latestCommentAuthorType,
938
1008
  latestCommentIsBot: input.snapshot.latestCommentIsBot,
1009
+ latestCommentBody: input.snapshot.latestCommentBody,
1010
+ latestCommentExcerpt,
1011
+ latestCommentUrl: input.snapshot.latestCommentUrl,
1012
+ latestCommentUpdatedAt: input.snapshot.latestCommentUpdatedAt,
939
1013
  failingChecks,
940
1014
  pendingChecks
941
1015
  }
942
1016
  }
943
1017
  };
1018
+ return notificationInput;
1019
+ }
1020
+ async #sendGithubNotification(input) {
1021
+ const notificationInput = this.#createGithubNotificationInput(input);
944
1022
  const streamOptions = await this.#agentOptions.getNotificationStreamOptions?.(input.target);
945
1023
  await input.agent?.sendNotificationSignal?.(
946
1024
  notificationInput,
@@ -959,25 +1037,144 @@ var GithubSignals = class extends signals.SignalProvider {
959
1037
  dedupeSuffix: `baseline:${input.subscription.lastSubscribeSignalId}`
960
1038
  });
961
1039
  }
962
- async #sendActivityNotification(input) {
1040
+ async #isAuthorizedAuthor(owner, repo, user, metadata = {}) {
1041
+ if (!user) return false;
1042
+ const normalizedUser = user.toLowerCase();
1043
+ const isBot = metadata.isBot === true || metadata.authorType?.toLowerCase() === "bot" || normalizedUser.endsWith("[bot]");
1044
+ if (isBot) {
1045
+ const ignoredBots = this.#options.ignoredBots ?? [];
1046
+ if (ignoredBots.some((bot) => bot.toLowerCase() === normalizedUser)) return false;
1047
+ const authorizedBots = this.#options.authorizedBots ?? DEFAULT_AUTHORIZED_BOTS;
1048
+ return authorizedBots.some((bot) => bot.toLowerCase() === normalizedUser);
1049
+ }
1050
+ const permission = await this.#loadAuthorPermission(owner, repo, user);
1051
+ const authorizedPermissions = this.#options.authorizedPermissions ?? DEFAULT_AUTHORIZED_PERMISSIONS;
1052
+ return !!permission && authorizedPermissions.includes(permission);
1053
+ }
1054
+ async #filterUnauthorizedLatestComment(owner, repo, snapshot) {
1055
+ const comments = snapshot.latestComments?.length ? snapshot.latestComments : [
1056
+ {
1057
+ author: snapshot.latestCommentAuthor,
1058
+ authorType: snapshot.latestCommentAuthorType,
1059
+ isBot: snapshot.latestCommentIsBot,
1060
+ body: snapshot.latestCommentBody,
1061
+ url: snapshot.latestCommentUrl,
1062
+ updatedAt: snapshot.latestCommentUpdatedAt
1063
+ }
1064
+ ];
1065
+ if (!comments.some((comment) => comment.author)) return snapshot;
1066
+ if (!comments.some((comment) => comment.body || comment.url || comment.updatedAt)) return snapshot;
1067
+ for (const comment of comments) {
1068
+ if (!await this.#isAuthorizedAuthor(owner, repo, comment.author, {
1069
+ authorType: comment.authorType,
1070
+ isBot: comment.isBot
1071
+ })) {
1072
+ continue;
1073
+ }
1074
+ return {
1075
+ ...snapshot,
1076
+ latestCommentAuthor: comment.author,
1077
+ latestCommentAuthorType: comment.authorType,
1078
+ latestCommentIsBot: comment.isBot,
1079
+ latestCommentBody: comment.body,
1080
+ latestCommentUrl: comment.url,
1081
+ latestCommentUpdatedAt: comment.updatedAt
1082
+ };
1083
+ }
1084
+ return {
1085
+ ...snapshot,
1086
+ latestCommentAuthor: void 0,
1087
+ latestCommentAuthorType: void 0,
1088
+ latestCommentIsBot: void 0,
1089
+ latestCommentBody: void 0,
1090
+ latestCommentUrl: void 0,
1091
+ latestCommentUpdatedAt: void 0
1092
+ };
1093
+ }
1094
+ async #loadAuthorPermission(owner, repo, user) {
1095
+ const cacheKey = `${owner}/${repo}:${user.toLowerCase()}`;
1096
+ const cached = this.#permissionCache.get(cacheKey);
1097
+ if (cached && cached.expiresAt > Date.now()) return cached.permission;
1098
+ if (cached) this.#permissionCache.delete(cacheKey);
1099
+ try {
1100
+ let permission;
1101
+ if (this.#options.permissionResolver) {
1102
+ permission = await this.#options.permissionResolver.getPermission(owner, repo, user);
1103
+ } else {
1104
+ const { stdout } = await execFileAsync("gh", [
1105
+ "api",
1106
+ `repos/${owner}/${repo}/collaborators/${user}/permission`,
1107
+ "--jq",
1108
+ ".permission"
1109
+ ]);
1110
+ const raw = stdout.trim();
1111
+ permission = ["admin", "maintain", "write", "triage", "read", "none"].includes(
1112
+ raw
1113
+ ) ? raw : void 0;
1114
+ }
1115
+ if (permission) {
1116
+ this.#permissionCache.set(cacheKey, { permission, expiresAt: Date.now() + PERMISSION_CACHE_TTL_MS });
1117
+ }
1118
+ return permission;
1119
+ } catch {
1120
+ this.#permissionCache.delete(cacheKey);
1121
+ return void 0;
1122
+ }
1123
+ }
1124
+ async #sendActivityNotifications(input) {
963
1125
  const agent = this.#getNotificationAgent(input.polling);
964
- if (!agent?.sendNotificationSignal) return void 0;
965
- const notification = classifyGithubActivityNotification({
966
- subscription: input.subscription,
967
- snapshot: input.snapshot
968
- });
969
- if (!notification) return void 0;
970
- await this.#sendGithubNotification({
971
- agent,
972
- subscription: input.subscription,
973
- snapshot: input.snapshot,
974
- notification,
975
- target: { resourceId: input.polling.resourceId, threadId: input.polling.threadId },
976
- dedupeSuffix: input.snapshot.contentHash ?? input.snapshot.githubUpdatedAt ?? String(Date.now()),
977
- previousGithubUpdatedAt: input.previousGithubUpdatedAt,
978
- previousContentHash: input.previousContentHash
979
- });
980
- return notification;
1126
+ if (!agent?.sendNotificationSignal) return [];
1127
+ const notifications = [
1128
+ classifyGithubActivityNotification({
1129
+ subscription: input.subscription,
1130
+ snapshot: input.snapshot
1131
+ })
1132
+ ];
1133
+ if (input.latestCommentChanged && notifications[0]?.kind !== "pull-request-activity") {
1134
+ notifications.push(
1135
+ classifyGithubCommentActivityNotification({
1136
+ subscription: input.subscription,
1137
+ snapshot: input.snapshot
1138
+ })
1139
+ );
1140
+ }
1141
+ const sent = [];
1142
+ const notificationInputs = [];
1143
+ for (const notification of notifications.sort(compareGithubActivityNotifications)) {
1144
+ if (!notification) continue;
1145
+ if (AUTHOR_GATED_NOTIFICATION_KINDS.has(notification.kind)) {
1146
+ const authorized = await this.#isAuthorizedAuthor(
1147
+ input.subscription.owner,
1148
+ input.subscription.repo,
1149
+ input.snapshot.latestCommentAuthor,
1150
+ {
1151
+ authorType: input.snapshot.latestCommentAuthorType,
1152
+ isBot: input.snapshot.latestCommentIsBot
1153
+ }
1154
+ );
1155
+ if (!authorized) continue;
1156
+ }
1157
+ notificationInputs.push(
1158
+ this.#createGithubNotificationInput({
1159
+ subscription: input.subscription,
1160
+ snapshot: input.snapshot,
1161
+ notification,
1162
+ dedupeSuffix: input.snapshot.contentHash ?? input.snapshot.githubUpdatedAt ?? String(Date.now()),
1163
+ previousGithubUpdatedAt: input.previousGithubUpdatedAt,
1164
+ previousContentHash: input.previousContentHash
1165
+ })
1166
+ );
1167
+ sent.push(notification);
1168
+ }
1169
+ if (notificationInputs.length > 0) {
1170
+ const target = { resourceId: input.polling.resourceId, threadId: input.polling.threadId };
1171
+ const streamOptions = await this.#agentOptions.getNotificationStreamOptions?.(target);
1172
+ await agent.sendNotificationSignal(
1173
+ notificationInputs,
1174
+ streamOptions ? { ...target, ifIdle: { streamOptions } } : target
1175
+ );
1176
+ }
1177
+ return sent;
981
1178
  }
982
1179
  async #subscribe(input) {
983
1180
  const { owner, repo } = await this.#resolveRepository(input);