patchrelay 0.25.1 → 0.25.3

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.25.1",
4
- "commit": "9c049524adf1",
5
- "builtAt": "2026-03-26T17:19:19.052Z"
3
+ "version": "0.25.3",
4
+ "commit": "f811135728dc",
5
+ "builtAt": "2026-03-26T19:17:03.821Z"
6
6
  }
@@ -1,4 +1,11 @@
1
1
  import { buildTimelineFromRehydration, appendFeedToTimeline, appendCodexItemToTimeline, completeCodexItemInTimeline, appendDeltaToTimelineItem, } from "./timeline-builder.js";
2
+ // ─── Array size caps (prevent OOM) ───────────────────────────────
3
+ const MAX_TIMELINE_ENTRIES = 2000;
4
+ const MAX_RAW_FEED_EVENTS = 2000;
5
+ const MAX_FEED_EVENTS = 1000;
6
+ function capArray(arr, max) {
7
+ return arr.length > max ? arr.slice(arr.length - max) : arr;
8
+ }
2
9
  const DETAIL_INITIAL = {
3
10
  detailTab: "timeline",
4
11
  timeline: [],
@@ -119,7 +126,7 @@ export function watchReducer(state, action) {
119
126
  case "feed-snapshot":
120
127
  return { ...state, feedEvents: action.events };
121
128
  case "feed-new-event":
122
- return { ...state, feedEvents: [...state.feedEvents, action.event] };
129
+ return { ...state, feedEvents: capArray([...state.feedEvents, action.event], MAX_FEED_EVENTS) };
123
130
  case "switch-detail-tab":
124
131
  return { ...state, detailTab: action.tab };
125
132
  }
@@ -157,10 +164,10 @@ function applyFeedEvent(state, event) {
157
164
  // Append to timeline and raw feed events if this event matches the active detail issue
158
165
  const isActiveDetail = state.view === "detail" && state.activeDetailKey === event.issueKey;
159
166
  const timeline = isActiveDetail
160
- ? appendFeedToTimeline(state.timeline, event)
167
+ ? capArray(appendFeedToTimeline(state.timeline, event), MAX_TIMELINE_ENTRIES)
161
168
  : state.timeline;
162
169
  const rawFeedEvents = isActiveDetail
163
- ? [...state.rawFeedEvents, event]
170
+ ? capArray([...state.rawFeedEvents, event], MAX_RAW_FEED_EVENTS)
164
171
  : state.rawFeedEvents;
165
172
  return { ...state, issues: updated, timeline, rawFeedEvents };
166
173
  }
@@ -168,7 +175,7 @@ function applyFeedEvent(state, event) {
168
175
  function applyCodexNotification(state, method, params) {
169
176
  switch (method) {
170
177
  case "item/started":
171
- return { ...state, timeline: appendCodexItemToTimeline(state.timeline, params, state.activeRunId) };
178
+ return { ...state, timeline: capArray(appendCodexItemToTimeline(state.timeline, params, state.activeRunId), MAX_TIMELINE_ENTRIES) };
172
179
  case "item/completed":
173
180
  return { ...state, timeline: completeCodexItemInTimeline(state.timeline, params) };
174
181
  case "item/agentMessage/delta":
@@ -305,6 +305,13 @@ export class WebhookHandler {
305
305
  }
306
306
  if (!triggerEventAllowed(project, normalized.triggerEvent))
307
307
  return;
308
+ // Ignore PatchRelay's own comments to prevent self-triggering feedback loops.
309
+ // When a run completes, PatchRelay posts an activity to Linear, which fires a
310
+ // commentCreated webhook back — without this guard that re-enqueues a new run.
311
+ const installation = this.db.linearInstallations.getLinearInstallationForProject(project.id);
312
+ if (installation?.actorId && normalized.actor?.id === installation.actorId) {
313
+ return;
314
+ }
308
315
  const issue = this.db.getIssue(project.id, normalized.issue.id);
309
316
  if (!issue)
310
317
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.25.1",
3
+ "version": "0.25.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {