patchrelay 0.83.1 → 0.83.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.83.1",
4
- "commit": "87265ce29424",
5
- "builtAt": "2026-06-14T18:10:49.183Z"
3
+ "version": "0.83.3",
4
+ "commit": "359ce862a979",
5
+ "builtAt": "2026-06-14T19:16:09.412Z"
6
6
  }
@@ -72,8 +72,10 @@ export function deriveLinkedPrAdoptionOutcome(project, prNumber, remote) {
72
72
  delegatedToPatchRelay: true,
73
73
  prNumber,
74
74
  prState,
75
+ prHeadSha: remote.headRefOid,
75
76
  prReviewState: reviewState,
76
77
  prCheckStatus: gateCheckStatus,
78
+ lastBlockingReviewHeadSha: reviewState === "changes_requested" ? remote.headRefOid : undefined,
77
79
  mergeConflictDetected,
78
80
  downstreamOwned,
79
81
  });
@@ -373,8 +373,10 @@ export class IdleIssueReconciler {
373
373
  const reactiveIntent = deriveIssueSessionReactiveIntent({
374
374
  prNumber: issue.prNumber,
375
375
  prState: issue.prState,
376
+ prHeadSha: issue.prHeadSha,
376
377
  prReviewState: issue.prReviewState,
377
378
  prCheckStatus: issue.prCheckStatus,
379
+ lastBlockingReviewHeadSha: issue.lastBlockingReviewHeadSha,
378
380
  latestFailureSource: issue.lastGitHubFailureSource,
379
381
  });
380
382
  if (!reactiveIntent && issue.factoryState === "awaiting_queue") {
@@ -705,8 +707,10 @@ export class IdleIssueReconciler {
705
707
  const reactiveIntent = deriveIssueSessionReactiveIntent({
706
708
  prNumber: refreshedIssue.prNumber,
707
709
  prState: refreshedIssue.prState,
710
+ prHeadSha: refreshedIssue.prHeadSha,
708
711
  prReviewState: refreshedIssue.prReviewState,
709
712
  prCheckStatus: refreshedIssue.prCheckStatus,
713
+ lastBlockingReviewHeadSha: refreshedIssue.lastBlockingReviewHeadSha,
710
714
  latestFailureSource: refreshedIssue.lastGitHubFailureSource,
711
715
  mergeConflictDetected,
712
716
  downstreamOwned,
@@ -133,8 +133,10 @@ export class IssueOverviewQuery {
133
133
  orchestrationSettleUntil: issueRecord?.orchestrationSettleUntil,
134
134
  ...(session.prNumber !== undefined ? { prNumber: session.prNumber } : {}),
135
135
  ...(issueRecord?.prState ? { prState: issueRecord.prState } : {}),
136
+ ...(issueRecord?.prHeadSha ? { prHeadSha: issueRecord.prHeadSha } : {}),
136
137
  ...(issueRecord?.prReviewState ? { prReviewState: issueRecord.prReviewState } : {}),
137
138
  ...(issueRecord?.prCheckStatus ? { prCheckStatus: issueRecord.prCheckStatus } : {}),
139
+ ...(issueRecord?.lastBlockingReviewHeadSha ? { lastBlockingReviewHeadSha: issueRecord.lastBlockingReviewHeadSha } : {}),
138
140
  ...(issueRecord?.lastGitHubFailureSource ? { latestFailureSource: issueRecord.lastGitHubFailureSource } : {}),
139
141
  }),
140
142
  ...(issueRecord?.lastGitHubFailureSource ? { latestFailureSource: issueRecord.lastGitHubFailureSource } : {}),
@@ -223,6 +223,9 @@ export function deriveSessionWakePlan(issue, events, onPayloadError) {
223
223
  }
224
224
  break;
225
225
  case "review_changes_requested":
226
+ if (isStaleRequestedChangesEvent(issue, typed.payload)) {
227
+ break;
228
+ }
226
229
  if (runType !== "queue_repair" && runType !== "ci_repair") {
227
230
  runType = typed.payload?.branchUpkeepRequired === true ? "branch_upkeep" : "review_fix";
228
231
  wakeReason = typed.payload?.branchUpkeepRequired === true ? "branch_upkeep" : "review_changes_requested";
@@ -356,6 +359,14 @@ export function deriveSessionWakePlan(issue, events, onPayloadError) {
356
359
  }
357
360
  return { eventIds, runType, wakeReason, resumeThread, context };
358
361
  }
362
+ function isStaleRequestedChangesEvent(issue, payload) {
363
+ if (payload?.branchUpkeepRequired === true)
364
+ return false;
365
+ const requestedChangesHeadSha = payload?.requestedChangesHeadSha;
366
+ if (!requestedChangesHeadSha || !issue.prHeadSha)
367
+ return false;
368
+ return requestedChangesHeadSha !== issue.prHeadSha;
369
+ }
359
370
  export function isActionableIssueSessionEventType(eventType) {
360
371
  return !NON_ACTIONABLE_SESSION_EVENTS.has(eventType);
361
372
  }
@@ -28,8 +28,10 @@ export function deriveIssueSessionWakeReason(params) {
28
28
  delegatedToPatchRelay: params.delegatedToPatchRelay,
29
29
  prNumber: params.prNumber,
30
30
  prState: params.prState,
31
+ prHeadSha: params.prHeadSha,
31
32
  prReviewState: params.prReviewState,
32
33
  prCheckStatus: params.prCheckStatus,
34
+ lastBlockingReviewHeadSha: params.lastBlockingReviewHeadSha,
33
35
  latestFailureSource: params.latestFailureSource,
34
36
  });
35
37
  if (reactiveIntent)
@@ -61,7 +63,11 @@ export function deriveIssueSessionReactiveIntent(params) {
61
63
  compatibilityFactoryState: "repairing_ci",
62
64
  };
63
65
  }
64
- if (params.prReviewState === "changes_requested") {
66
+ if (isCurrentHeadRequestedChanges({
67
+ prReviewState: params.prReviewState,
68
+ prHeadSha: params.prHeadSha,
69
+ lastBlockingReviewHeadSha: params.lastBlockingReviewHeadSha,
70
+ })) {
65
71
  if (params.mergeConflictDetected) {
66
72
  return {
67
73
  runType: "branch_upkeep",
@@ -106,8 +112,10 @@ export function isIssueSessionReadyForExecution(params) {
106
112
  delegatedToPatchRelay: params.delegatedToPatchRelay,
107
113
  prNumber: params.prNumber,
108
114
  prState: params.prState,
115
+ prHeadSha: params.prHeadSha,
109
116
  prReviewState: params.prReviewState,
110
117
  prCheckStatus: params.prCheckStatus,
118
+ lastBlockingReviewHeadSha: params.lastBlockingReviewHeadSha,
111
119
  latestFailureSource: params.latestFailureSource,
112
120
  }) === undefined) {
113
121
  return false;
@@ -121,3 +129,10 @@ export function isIssueSessionReadyForExecution(params) {
121
129
  }
122
130
  return true;
123
131
  }
132
+ export function isCurrentHeadRequestedChanges(params) {
133
+ if (params.prReviewState !== "changes_requested")
134
+ return false;
135
+ if (!params.lastBlockingReviewHeadSha || !params.prHeadSha)
136
+ return true;
137
+ return params.lastBlockingReviewHeadSha === params.prHeadSha;
138
+ }
@@ -208,8 +208,10 @@ function resolveOpenWorkflowState(issue) {
208
208
  delegatedToPatchRelay: issue.delegatedToPatchRelay,
209
209
  prNumber: issue.prNumber,
210
210
  prState: issue.prState,
211
+ prHeadSha: issue.prHeadSha,
211
212
  prReviewState: issue.prReviewState,
212
213
  prCheckStatus: issue.prCheckStatus,
214
+ lastBlockingReviewHeadSha: issue.lastBlockingReviewHeadSha,
213
215
  latestFailureSource: issue.lastGitHubFailureSource,
214
216
  });
215
217
  if (reactiveIntent) {
@@ -35,8 +35,10 @@ export function resolvePostRunFactoryState(issue, _run, options) {
35
35
  const reactiveIntent = deriveIssueSessionReactiveIntent({
36
36
  prNumber: issue.prNumber,
37
37
  prState: issue.prState,
38
+ prHeadSha: issue.prHeadSha,
38
39
  prReviewState: issue.prReviewState,
39
40
  prCheckStatus: issue.prCheckStatus,
41
+ lastBlockingReviewHeadSha: issue.lastBlockingReviewHeadSha,
40
42
  latestFailureSource: issue.lastGitHubFailureSource,
41
43
  });
42
44
  if (reactiveIntent)
@@ -125,8 +125,10 @@ export class ServiceStartupRecovery {
125
125
  prNumber: issue.prNumber,
126
126
  prState: issue.prState,
127
127
  prIsDraft: issue.prIsDraft,
128
+ prHeadSha: issue.prHeadSha,
128
129
  prReviewState: issue.prReviewState,
129
130
  prCheckStatus: issue.prCheckStatus,
131
+ lastBlockingReviewHeadSha: issue.lastBlockingReviewHeadSha,
130
132
  latestFailureSource: issue.lastGitHubFailureSource,
131
133
  })
132
134
  : undefined;
package/dist/service.js CHANGED
@@ -20,6 +20,15 @@ import { parseStringArray, TrackedIssueListQuery } from "./tracked-issue-list-qu
20
20
  import { AgentInputService } from "./agent-input-service.js";
21
21
  import { CodexFollowupIntentClassifier } from "./followup-intent.js";
22
22
  import { FanoutPatchRelayTelemetry, LoggerTelemetrySink, OperatorFeedTelemetrySink } from "./telemetry.js";
23
+ function readPositiveIntegerEnv(name) {
24
+ const raw = process.env[name]?.trim();
25
+ if (!raw)
26
+ return undefined;
27
+ const value = Number(raw);
28
+ if (!Number.isFinite(value) || value < 1)
29
+ return undefined;
30
+ return Math.floor(value);
31
+ }
23
32
  export class PatchRelayService {
24
33
  config;
25
34
  db;
@@ -70,6 +79,18 @@ export class PatchRelayService {
70
79
  leaseRelease = (projectId, issueId) => this.orchestrator.leaseService.release(projectId, issueId);
71
80
  this.webhookHandler = new WebhookHandler(config, db, this.linearProvider, codex, dispatcher, logger, this.feed, undefined, agentInput, telemetry);
72
81
  this.githubWebhookHandler = new GitHubWebhookHandler(config, db, this.linearProvider, dispatcher, logger, codex, this.feed);
82
+ const runtimeOptions = {
83
+ assertStorageReady: () => db.assertSchemaReady(),
84
+ describeStorage: () => db.describeSchema(),
85
+ };
86
+ const maxActiveIssueRuns = readPositiveIntegerEnv("PATCHRELAY_MAX_ACTIVE_ISSUE_RUNS");
87
+ if (maxActiveIssueRuns !== undefined) {
88
+ runtimeOptions.maxActiveIssueRuns = maxActiveIssueRuns;
89
+ }
90
+ const issueRunCapacityRetryDelayMs = readPositiveIntegerEnv("PATCHRELAY_ISSUE_RUN_CAPACITY_RETRY_DELAY_MS");
91
+ if (issueRunCapacityRetryDelayMs !== undefined) {
92
+ runtimeOptions.issueRunCapacityRetryDelayMs = issueRunCapacityRetryDelayMs;
93
+ }
73
94
  const runtime = new ServiceRuntime(codex, logger, this.orchestrator, {
74
95
  listIssuesReadyForExecution: () => db.listIssuesReadyForExecution(),
75
96
  countActiveIssueRuns: () => db.runs.listActiveRuns().length,
@@ -77,10 +98,7 @@ export class PatchRelayService {
77
98
  processIssue: async (item) => {
78
99
  await this.orchestrator.run(item);
79
100
  },
80
- }, {
81
- assertStorageReady: () => db.assertSchemaReady(),
82
- describeStorage: () => db.describeSchema(),
83
- });
101
+ }, runtimeOptions);
84
102
  enqueueIssue = (projectId, issueId) => runtime.enqueueIssue(projectId, issueId);
85
103
  this.oauthService = new LinearOAuthService(config, { linearInstallations: db.linearInstallations }, logger);
86
104
  this.queryService = new IssueQueryService(db, codex, this.orchestrator);
@@ -75,8 +75,10 @@ export function buildTrackedIssueRecord(params) {
75
75
  orchestrationSettleUntil: params.issue.orchestrationSettleUntil,
76
76
  ...(params.issue.prNumber !== undefined ? { prNumber: params.issue.prNumber } : {}),
77
77
  ...(params.issue.prState ? { prState: params.issue.prState } : {}),
78
+ ...(params.issue.prHeadSha ? { prHeadSha: params.issue.prHeadSha } : {}),
78
79
  ...(params.issue.prReviewState ? { prReviewState: params.issue.prReviewState } : {}),
79
80
  ...(params.issue.prCheckStatus ? { prCheckStatus: params.issue.prCheckStatus } : {}),
81
+ ...(params.issue.lastBlockingReviewHeadSha ? { lastBlockingReviewHeadSha: params.issue.lastBlockingReviewHeadSha } : {}),
80
82
  ...(params.issue.lastGitHubFailureSource ? { latestFailureSource: params.issue.lastGitHubFailureSource } : {}),
81
83
  }),
82
84
  ...(params.issue.lastGitHubFailureSource ? { latestFailureSource: params.issue.lastGitHubFailureSource } : {}),
@@ -49,8 +49,10 @@ export function resolveReDelegationResume(p) {
49
49
  prNumber: p.prNumber,
50
50
  prState: p.prState,
51
51
  prIsDraft: p.prIsDraft,
52
+ prHeadSha: p.prHeadSha,
52
53
  prReviewState: p.prReviewState,
53
54
  prCheckStatus: p.prCheckStatus,
55
+ lastBlockingReviewHeadSha: p.lastBlockingReviewHeadSha,
54
56
  latestFailureSource: p.latestFailureSource,
55
57
  });
56
58
  if (reactiveIntent) {
@@ -1,4 +1,5 @@
1
1
  import { buildFailureContext } from "./idle-reconciliation-helpers.js";
2
+ import { isCurrentHeadRequestedChanges } from "./issue-session.js";
2
3
  import { tryParseRunContextValue } from "./run-context.js";
3
4
  function parseObservationPayload(observation) {
4
5
  if (!observation.payloadJson)
@@ -245,7 +246,11 @@ export function deriveWorkflowTasks(snapshot) {
245
246
  requirements: { childCount: snapshot.childCount },
246
247
  }];
247
248
  }
248
- if (prState === "open" && prReviewState === "changes_requested") {
249
+ if (prState === "open" && isCurrentHeadRequestedChanges({
250
+ prReviewState: typeof prReviewState === "string" ? prReviewState : undefined,
251
+ prHeadSha: typeof prHeadSha === "string" ? prHeadSha : undefined,
252
+ lastBlockingReviewHeadSha: issue.lastBlockingReviewHeadSha,
253
+ })) {
249
254
  tasks.push({
250
255
  id: "run:review_fix",
251
256
  type: "run",
@@ -38,8 +38,10 @@ export function deriveImplicitReactiveWake(issue) {
38
38
  activeRunId: issue.activeRunId,
39
39
  prNumber: issue.prNumber,
40
40
  prState: issue.prState,
41
+ prHeadSha: issue.prHeadSha,
41
42
  prReviewState: issue.prReviewState,
42
43
  prCheckStatus: issue.prCheckStatus,
44
+ lastBlockingReviewHeadSha: issue.lastBlockingReviewHeadSha,
43
45
  latestFailureSource: issue.lastGitHubFailureSource,
44
46
  });
45
47
  if (!reactiveIntent)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.83.1",
3
+ "version": "0.83.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {