patchrelay 0.35.17 → 0.36.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.
@@ -585,38 +585,55 @@ export class WebhookHandler {
585
585
  }
586
586
  if (!triggerEventAllowed(project, normalized.triggerEvent))
587
587
  return;
588
- // Ignore PatchRelay's own comments to prevent self-triggering feedback loops.
589
- // When a run completes, PatchRelay posts an activity to Linear, which fires a
590
- // commentCreated webhook back — without this guard that re-enqueues a new run.
588
+ const issue = this.db.getIssue(project.id, normalized.issue.id);
589
+ if (!issue)
590
+ return;
591
+ const trimmedBody = normalized.comment.body.trim();
592
+ // Ignore PatchRelay-managed comments to prevent status-sync feedback loops.
593
+ // Linear commentUpdated/commentCreated events can arrive after PatchRelay
594
+ // refreshes its visible status comment, and those updates should never
595
+ // consume review-fix budget or wake a new run.
591
596
  const installation = this.db.linearInstallations.getLinearInstallationForProject(project.id);
592
- if (installation?.actorId && normalized.actor?.id === installation.actorId) {
597
+ const selfAuthored = this.isPatchRelayManagedCommentAuthor(installation, normalized.actor, normalized.comment.userName);
598
+ const inertPatchRelayComment = this.isInertPatchRelayComment(issue, normalized.comment.id, trimmedBody, normalized.actor?.type);
599
+ if (selfAuthored || inertPatchRelayComment) {
593
600
  this.db.appendIssueSessionEventRespectingActiveLease(project.id, normalized.issue.id, {
594
601
  projectId: project.id,
595
602
  linearIssueId: normalized.issue.id,
596
603
  eventType: "self_comment",
597
604
  eventJson: JSON.stringify({
598
- body: normalized.comment.body.trim(),
605
+ body: trimmedBody,
599
606
  author: normalized.comment.userName,
600
607
  }),
601
608
  });
602
609
  return;
603
610
  }
604
- const issue = this.db.getIssue(project.id, normalized.issue.id);
605
- if (!issue)
606
- return;
607
611
  // No active run — enqueue a run with the comment as context if appropriate
608
612
  if (!issue.activeRunId) {
609
613
  const ENQUEUEABLE_STATES = new Set(["pr_open", "changes_requested", "implementing", "delegated", "awaiting_input"]);
610
614
  if (ENQUEUEABLE_STATES.has(issue.factoryState)) {
615
+ const directReply = this.isDirectReplyToOutstandingQuestion(issue);
616
+ const wakeIntent = directReply || this.hasExplicitPatchRelayWakeIntent(trimmedBody);
617
+ if (!wakeIntent) {
618
+ this.feed?.publish({
619
+ level: "info",
620
+ kind: "comment",
621
+ projectId: project.id,
622
+ issueKey: trackedIssue?.issueKey,
623
+ status: "ignored",
624
+ summary: "Ignored comment with no explicit PatchRelay wake intent",
625
+ detail: trimmedBody.slice(0, 200),
626
+ });
627
+ return;
628
+ }
611
629
  const runType = issue.prReviewState === "changes_requested" ? "review_fix" : "implementation";
612
630
  const hadPendingWake = this.db.peekIssueSessionWake(project.id, normalized.issue.id) !== undefined;
613
- const directReply = this.isDirectReplyToOutstandingQuestion(issue);
614
631
  this.db.appendIssueSessionEventRespectingActiveLease(project.id, normalized.issue.id, {
615
632
  projectId: project.id,
616
633
  linearIssueId: normalized.issue.id,
617
634
  eventType: directReply ? "direct_reply" : "followup_comment",
618
635
  eventJson: JSON.stringify({
619
- body: normalized.comment.body.trim(),
636
+ body: trimmedBody,
620
637
  author: normalized.comment.userName,
621
638
  }),
622
639
  });
@@ -630,7 +647,7 @@ export class WebhookHandler {
630
647
  issueKey: trackedIssue?.issueKey,
631
648
  status: "enqueued",
632
649
  summary: `Comment enqueued ${(queuedRunType ?? runType)} run`,
633
- detail: normalized.comment.body.slice(0, 200),
650
+ detail: trimmedBody.slice(0, 200),
634
651
  });
635
652
  }
636
653
  return;
@@ -642,7 +659,7 @@ export class WebhookHandler {
642
659
  "New Linear comment received while you are working.",
643
660
  normalized.comment.userName ? `Author: ${normalized.comment.userName}` : undefined,
644
661
  "",
645
- normalized.comment.body.trim(),
662
+ trimmedBody,
646
663
  ].filter(Boolean).join("\n");
647
664
  try {
648
665
  await this.codex.steerTurn({ threadId: run.threadId, turnId: run.turnId, input: body });
@@ -665,7 +682,7 @@ export class WebhookHandler {
665
682
  linearIssueId: normalized.issue.id,
666
683
  eventType: directReply ? "direct_reply" : "followup_comment",
667
684
  eventJson: JSON.stringify({
668
- body: normalized.comment.body.trim(),
685
+ body: trimmedBody,
669
686
  author: normalized.comment.userName,
670
687
  }),
671
688
  });
@@ -683,6 +700,47 @@ export class WebhookHandler {
683
700
  });
684
701
  }
685
702
  }
703
+ isInertPatchRelayComment(issue, commentId, body, actorType) {
704
+ if (commentId === issue.statusCommentId) {
705
+ return true;
706
+ }
707
+ if (body.startsWith("## PatchRelay status")
708
+ && body.includes("_PatchRelay updates this comment as it works. Review and merge remain downstream._")) {
709
+ return true;
710
+ }
711
+ const normalizedActorType = actorType?.trim().toLowerCase();
712
+ if (normalizedActorType && normalizedActorType !== "user") {
713
+ return this.isPatchRelayGeneratedActivityComment(body);
714
+ }
715
+ return false;
716
+ }
717
+ isPatchRelayManagedCommentAuthor(installation, actor, commentUserName) {
718
+ const actorName = actor?.name?.trim().toLowerCase();
719
+ const commentAuthor = commentUserName?.trim().toLowerCase();
720
+ const installationName = installation?.actorName?.trim().toLowerCase();
721
+ if (installation?.actorId && actor?.id === installation.actorId) {
722
+ return true;
723
+ }
724
+ if (installationName && actorName === installationName) {
725
+ return true;
726
+ }
727
+ if (actorName === "patchrelay" || commentAuthor === "patchrelay") {
728
+ return true;
729
+ }
730
+ return false;
731
+ }
732
+ isPatchRelayGeneratedActivityComment(body) {
733
+ return body.startsWith("PatchRelay needs human help to continue.")
734
+ || body.startsWith("PatchRelay is already working on ")
735
+ || body.startsWith("PatchRelay received the ")
736
+ || body.startsWith("PatchRelay routed your latest instructions into ")
737
+ || body.startsWith("PatchRelay has stopped work as requested.")
738
+ || body.startsWith("Merge preparation failed ")
739
+ || body === "This thread is for an agent session with patchrelay.";
740
+ }
741
+ hasExplicitPatchRelayWakeIntent(body) {
742
+ return /\bpatchrelay\b/i.test(body);
743
+ }
686
744
  peekPendingSessionWakeRunType(projectId, issueId) {
687
745
  return this.db.peekIssueSessionWake(projectId, issueId)?.runType;
688
746
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.35.17",
3
+ "version": "0.36.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {