patchrelay 0.69.3 → 0.69.4

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.69.3",
4
- "commit": "be06a08a368b",
5
- "builtAt": "2026-05-22T21:56:51.986Z"
3
+ "version": "0.69.4",
4
+ "commit": "56b3535ee459",
5
+ "builtAt": "2026-05-22T22:23:51.490Z"
6
6
  }
@@ -6,14 +6,12 @@ export class IssueSessionStore {
6
6
  mapIssueSessionEventRow;
7
7
  issues;
8
8
  runs;
9
- deriveImplicitReactiveWake;
10
- constructor(connection, mapIssueSessionRow, mapIssueSessionEventRow, issues, runs, deriveImplicitReactiveWake) {
9
+ constructor(connection, mapIssueSessionRow, mapIssueSessionEventRow, issues, runs) {
11
10
  this.connection = connection;
12
11
  this.mapIssueSessionRow = mapIssueSessionRow;
13
12
  this.mapIssueSessionEventRow = mapIssueSessionEventRow;
14
13
  this.issues = issues;
15
14
  this.runs = runs;
16
- this.deriveImplicitReactiveWake = deriveImplicitReactiveWake;
17
15
  }
18
16
  getIssueSession(projectId, linearIssueId) {
19
17
  const row = this.connection
@@ -108,16 +106,7 @@ export class IssueSessionStore {
108
106
  resumeThread: plan.resumeThread,
109
107
  };
110
108
  }
111
- const implicitWake = this.deriveImplicitReactiveWake(issue);
112
- if (!implicitWake)
113
- return undefined;
114
- return {
115
- eventIds: [],
116
- runType: implicitWake.runType,
117
- context: implicitWake.context,
118
- wakeReason: implicitWake.wakeReason,
119
- resumeThread: false,
120
- };
109
+ return undefined;
121
110
  }
122
111
  acquireIssueSessionLease(params) {
123
112
  const now = params.now ?? isoNow();
package/dist/db.js CHANGED
@@ -1,4 +1,3 @@
1
- import { deriveIssueSessionReactiveIntent, } from "./issue-session.js";
2
1
  import {} from "./issue-session-events.js";
3
2
  import { IssueStore } from "./db/issue-store.js";
4
3
  import { IssueSessionStore } from "./db/issue-session-store.js";
@@ -11,84 +10,7 @@ import { runPatchRelayMigrations } from "./db/migrations.js";
11
10
  import { SqliteConnection } from "./db/shared.js";
12
11
  import { syncIssueSessionFromIssue } from "./issue-session-projector.js";
13
12
  import { TrackedIssueQuery } from "./tracked-issue-query.js";
14
- function parseObjectJson(raw) {
15
- if (!raw)
16
- return undefined;
17
- try {
18
- const parsed = JSON.parse(raw);
19
- return parsed && typeof parsed === "object" && !Array.isArray(parsed)
20
- ? parsed
21
- : undefined;
22
- }
23
- catch {
24
- return undefined;
25
- }
26
- }
27
- function hasUnattemptedFailureSignature(issue, fallbackHeadSha) {
28
- const signature = issue.lastGitHubFailureSignature;
29
- if (!signature)
30
- return false;
31
- const headSha = issue.lastGitHubFailureHeadSha ?? fallbackHeadSha;
32
- return issue.lastAttemptedFailureSignature !== signature
33
- || (headSha !== undefined && issue.lastAttemptedFailureHeadSha !== headSha);
34
- }
35
- function deriveImplicitReactiveWake(issue) {
36
- const reactiveIntent = deriveIssueSessionReactiveIntent({
37
- delegatedToPatchRelay: issue.delegatedToPatchRelay,
38
- activeRunId: issue.activeRunId,
39
- prNumber: issue.prNumber,
40
- prState: issue.prState,
41
- prReviewState: issue.prReviewState,
42
- prCheckStatus: issue.prCheckStatus,
43
- latestFailureSource: issue.lastGitHubFailureSource,
44
- });
45
- if (!reactiveIntent)
46
- return undefined;
47
- if (reactiveIntent.runType === "ci_repair") {
48
- const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
49
- const snapshot = parseObjectJson(issue.lastGitHubCiSnapshotJson);
50
- const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
51
- ? failureContext.failureHeadSha
52
- : issue.lastGitHubFailureHeadSha ?? issue.prHeadSha;
53
- const failureSignature = issue.lastGitHubFailureSignature
54
- ?? (fallbackHeadSha ? `implicit_branch_ci::${fallbackHeadSha}` : undefined);
55
- if (!failureSignature || issue.prState !== "open")
56
- return undefined;
57
- if (issue.lastAttemptedFailureSignature === failureSignature
58
- && (fallbackHeadSha === undefined || issue.lastAttemptedFailureHeadSha === fallbackHeadSha)) {
59
- return undefined;
60
- }
61
- return {
62
- runType: reactiveIntent.runType,
63
- wakeReason: reactiveIntent.wakeReason,
64
- context: {
65
- ...failureContext,
66
- failureSignature,
67
- ...(fallbackHeadSha ? { failureHeadSha: fallbackHeadSha } : {}),
68
- ...(issue.lastGitHubFailureCheckName ? { checkName: issue.lastGitHubFailureCheckName } : {}),
69
- ...(snapshot ? { ciSnapshot: snapshot } : {}),
70
- },
71
- };
72
- }
73
- if (reactiveIntent.runType === "queue_repair") {
74
- const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
75
- const incidentContext = parseObjectJson(issue.lastQueueIncidentJson) ?? {};
76
- const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
77
- ? failureContext.failureHeadSha
78
- : undefined;
79
- if (!hasUnattemptedFailureSignature(issue, fallbackHeadSha))
80
- return undefined;
81
- return {
82
- runType: reactiveIntent.runType,
83
- wakeReason: reactiveIntent.wakeReason,
84
- context: {
85
- ...incidentContext,
86
- ...failureContext,
87
- },
88
- };
89
- }
90
- return undefined;
91
- }
13
+ import { WorkflowWakeResolver } from "./workflow-wake-resolver.js";
92
14
  export class PatchRelayDatabase {
93
15
  connection;
94
16
  linearInstallations;
@@ -97,6 +19,7 @@ export class PatchRelayDatabase {
97
19
  webhookEvents;
98
20
  issues;
99
21
  issueSessions;
22
+ workflowWakes;
100
23
  runs;
101
24
  trackedIssues;
102
25
  constructor(databasePath, wal) {
@@ -118,8 +41,9 @@ export class PatchRelayDatabase {
118
41
  issue,
119
42
  ...(options ? { options } : {}),
120
43
  }));
121
- this.issueSessions = new IssueSessionStore(this.connection, mapIssueSessionRow, mapIssueSessionEventRow, this.issues, this.runs, deriveImplicitReactiveWake);
122
- this.trackedIssues = new TrackedIssueQuery(this.issues, this.issueSessions, this.runs);
44
+ this.issueSessions = new IssueSessionStore(this.connection, mapIssueSessionRow, mapIssueSessionEventRow, this.issues, this.runs);
45
+ this.workflowWakes = new WorkflowWakeResolver(this.issues, this.issueSessions);
46
+ this.trackedIssues = new TrackedIssueQuery(this.issues, this.issueSessions, this.workflowWakes, this.runs);
123
47
  }
124
48
  runMigrations() {
125
49
  runPatchRelayMigrations(this.connection);
@@ -513,7 +513,7 @@ export class IdleIssueReconciler {
513
513
  }
514
514
  if (issue.delegatedToPatchRelay
515
515
  && reactiveIntent?.runType === "review_fix"
516
- && this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) === undefined) {
516
+ && this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId) === undefined) {
517
517
  this.logger.info({
518
518
  issueKey: issue.issueKey,
519
519
  prNumber: issue.prNumber,
@@ -147,7 +147,7 @@ export class InterruptedRunRecovery {
147
147
  eventType: "delegated",
148
148
  dedupeKey: `interrupted_implementation:implementation:${run.linearIssueId}`,
149
149
  });
150
- if (!this.db.issueSessions.peekIssueSessionWake(run.projectId, run.linearIssueId)) {
150
+ if (!this.db.workflowWakes.peekIssueWake(run.projectId, run.linearIssueId)) {
151
151
  const failedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? refreshedIssue;
152
152
  this.feed?.publish({
153
153
  level: "error",
@@ -127,7 +127,7 @@ export class IssueOverviewQuery {
127
127
  delegatedToPatchRelay: issueRecord?.delegatedToPatchRelay,
128
128
  ...(activeRun ? { activeRunId: activeRun.id } : {}),
129
129
  blockedByCount: unresolvedBlockedBy.length,
130
- hasPendingWake: this.db.issueSessions.peekIssueSessionWake(session.projectId, session.linearIssueId) !== undefined,
130
+ hasPendingWake: this.db.workflowWakes.peekIssueWake(session.projectId, session.linearIssueId) !== undefined,
131
131
  hasLegacyPendingRun: issueRecord?.pendingRunType !== undefined,
132
132
  orchestrationSettleUntil: issueRecord?.orchestrationSettleUntil,
133
133
  ...(session.prNumber !== undefined ? { prNumber: session.prNumber } : {}),
@@ -5,7 +5,7 @@ export class RunWakePlanner {
5
5
  this.db = db;
6
6
  }
7
7
  resolveRunWake(issue) {
8
- const sessionWake = this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId);
8
+ const sessionWake = this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId);
9
9
  if (!sessionWake)
10
10
  return undefined;
11
11
  return {
@@ -134,7 +134,7 @@ export class ServiceIssueActions {
134
134
  status: "retry",
135
135
  summary: `Retry queued: ${retryTarget.runType}`,
136
136
  });
137
- if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
137
+ if (this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId)) {
138
138
  this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
139
139
  }
140
140
  return { issueKey, runType: retryTarget.runType };
@@ -90,7 +90,7 @@ export class ServiceStartupRecovery {
90
90
  }
91
91
  const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
92
92
  const latestRun = this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
93
- const hasPendingWake = this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined;
93
+ const hasPendingWake = this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId) !== undefined;
94
94
  const shouldRecoverPausedLocalWork = delegated
95
95
  && isResumablePausedLocalWork({
96
96
  issue: {
@@ -147,7 +147,7 @@ export class ServiceStartupRecovery {
147
147
  dedupeKey: `delegated:${issue.linearIssueId}`,
148
148
  });
149
149
  }
150
- if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
150
+ if (this.db.workflowWakes.peekIssueWake(issue.projectId, issue.linearIssueId)) {
151
151
  this.enqueueIssue(issue.projectId, issue.linearIssueId);
152
152
  }
153
153
  this.logger.info({
@@ -155,7 +155,7 @@ export class TrackedIssueListQuery {
155
155
  const blockedByCount = Number(row.blocked_by_count ?? 0);
156
156
  const hasPendingSessionEvents = Number(row.pending_session_event_count ?? 0) > 0;
157
157
  const hasPendingWake = hasPendingSessionEvents
158
- || this.db.issueSessions.peekIssueSessionWake(String(row.project_id), String(row.linear_issue_id)) !== undefined;
158
+ || this.db.workflowWakes.peekIssueWake(String(row.project_id), String(row.linear_issue_id)) !== undefined;
159
159
  const detachedActiveRun = hasDetachedActiveLatestRun({
160
160
  activeRunId: row.active_run_type !== null ? 1 : undefined,
161
161
  latestRun: row.latest_run_status !== null
@@ -4,10 +4,12 @@ import { resolveEffectiveActiveRun } from "./effective-active-run.js";
4
4
  export class TrackedIssueQuery {
5
5
  issues;
6
6
  issueSessions;
7
+ workflowWakes;
7
8
  runs;
8
- constructor(issues, issueSessions, runs) {
9
+ constructor(issues, issueSessions, workflowWakes, runs) {
9
10
  this.issues = issues;
10
11
  this.issueSessions = issueSessions;
12
+ this.workflowWakes = workflowWakes;
11
13
  this.runs = runs;
12
14
  }
13
15
  listIssuesReadyForExecution() {
@@ -20,7 +22,7 @@ export class TrackedIssueQuery {
20
22
  }),
21
23
  activeRunId: issue.activeRunId,
22
24
  blockedByCount: this.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId),
23
- hasPendingWake: this.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined,
25
+ hasPendingWake: this.workflowWakes.hasPendingWake(issue.projectId, issue.linearIssueId),
24
26
  hasLegacyPendingRun: issue.pendingRunType !== undefined,
25
27
  prNumber: issue.prNumber,
26
28
  prState: issue.prState,
@@ -38,7 +40,7 @@ export class TrackedIssueQuery {
38
40
  issue,
39
41
  session: this.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId),
40
42
  blockedBy: this.issues.listIssueDependencies(issue.projectId, issue.linearIssueId),
41
- hasPendingWake: this.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined,
43
+ hasPendingWake: this.workflowWakes.hasPendingWake(issue.projectId, issue.linearIssueId),
42
44
  latestRun: this.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId),
43
45
  latestEvent: this.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1),
44
46
  });
@@ -69,7 +69,7 @@ export class WakeDispatcher {
69
69
  const issue = this.db.issues.getIssue(projectId, linearIssueId);
70
70
  if (issue?.activeRunId !== undefined)
71
71
  return undefined;
72
- const wake = this.db.issueSessions.peekIssueSessionWake(projectId, linearIssueId);
72
+ const wake = this.db.workflowWakes.peekIssueWake(projectId, linearIssueId);
73
73
  // Fall back to the legacy pending_run_type column. The orchestrator
74
74
  // materializes it into a real event at run time, but the poke still
75
75
  // needs to happen now so the orchestrator gets called at all.
@@ -97,7 +97,7 @@ export class WakeDispatcher {
97
97
  // check paths publish their own more-specific event and pass false.
98
98
  releaseRunAndDispatch(params) {
99
99
  this.releaseLease(params.run.projectId, params.run.linearIssueId);
100
- const wake = this.db.issueSessions.peekIssueSessionWake(params.run.projectId, params.run.linearIssueId);
100
+ const wake = this.db.workflowWakes.peekIssueWake(params.run.projectId, params.run.linearIssueId);
101
101
  if (!wake)
102
102
  return undefined;
103
103
  this.enqueueIssue(params.run.projectId, params.run.linearIssueId);
@@ -232,7 +232,7 @@ export class WebhookHandler {
232
232
  }
233
233
  }
234
234
  peekPendingSessionWakeRunType(projectId, issueId) {
235
- return this.db.issueSessions.peekIssueSessionWake(projectId, issueId)?.runType;
235
+ return this.db.workflowWakes.peekIssueWake(projectId, issueId)?.runType;
236
236
  }
237
237
  enqueuePendingSessionWake(projectId, issueId) {
238
238
  return this.wakeDispatcher.dispatchIfWakePending(projectId, issueId);
@@ -29,7 +29,7 @@ export class DesiredStageRecorder {
29
29
  const latestRun = existingIssue ? this.db.runs.getLatestRunForIssue(params.project.id, normalizedIssue.id) : undefined;
30
30
  const triggerAllowed = triggerEventAllowed(params.project, params.normalized.triggerEvent);
31
31
  const incomingAgentSessionId = params.normalized.agentSession?.id;
32
- const hasPendingWake = this.db.issueSessions.peekIssueSessionWake(params.project.id, normalizedIssue.id) !== undefined;
32
+ const hasPendingWake = params.peekPendingSessionWakeRunType(params.project.id, normalizedIssue.id) !== undefined;
33
33
  if (!existingIssue && !isDelegatedToPatchRelay(this.db, params.project, normalizedIssue) && !incomingAgentSessionId) {
34
34
  return { issue: undefined, wakeRunType: undefined, delegated: false };
35
35
  }
@@ -0,0 +1,108 @@
1
+ import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
2
+ function parseObjectJson(raw) {
3
+ if (!raw)
4
+ return undefined;
5
+ try {
6
+ const parsed = JSON.parse(raw);
7
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
8
+ ? parsed
9
+ : undefined;
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
15
+ function hasUnattemptedFailureSignature(issue, fallbackHeadSha) {
16
+ const signature = issue.lastGitHubFailureSignature;
17
+ if (!signature)
18
+ return false;
19
+ const headSha = issue.lastGitHubFailureHeadSha ?? fallbackHeadSha;
20
+ return issue.lastAttemptedFailureSignature !== signature
21
+ || (headSha !== undefined && issue.lastAttemptedFailureHeadSha !== headSha);
22
+ }
23
+ export function deriveImplicitReactiveWake(issue) {
24
+ const reactiveIntent = deriveIssueSessionReactiveIntent({
25
+ delegatedToPatchRelay: issue.delegatedToPatchRelay,
26
+ activeRunId: issue.activeRunId,
27
+ prNumber: issue.prNumber,
28
+ prState: issue.prState,
29
+ prReviewState: issue.prReviewState,
30
+ prCheckStatus: issue.prCheckStatus,
31
+ latestFailureSource: issue.lastGitHubFailureSource,
32
+ });
33
+ if (!reactiveIntent)
34
+ return undefined;
35
+ if (reactiveIntent.runType === "ci_repair") {
36
+ const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
37
+ const snapshot = parseObjectJson(issue.lastGitHubCiSnapshotJson);
38
+ const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
39
+ ? failureContext.failureHeadSha
40
+ : issue.lastGitHubFailureHeadSha ?? issue.prHeadSha;
41
+ const failureSignature = issue.lastGitHubFailureSignature
42
+ ?? (fallbackHeadSha ? `implicit_branch_ci::${fallbackHeadSha}` : undefined);
43
+ if (!failureSignature || issue.prState !== "open")
44
+ return undefined;
45
+ if (issue.lastAttemptedFailureSignature === failureSignature
46
+ && (fallbackHeadSha === undefined || issue.lastAttemptedFailureHeadSha === fallbackHeadSha)) {
47
+ return undefined;
48
+ }
49
+ return {
50
+ runType: reactiveIntent.runType,
51
+ wakeReason: reactiveIntent.wakeReason,
52
+ context: {
53
+ ...failureContext,
54
+ failureSignature,
55
+ ...(fallbackHeadSha ? { failureHeadSha: fallbackHeadSha } : {}),
56
+ ...(issue.lastGitHubFailureCheckName ? { checkName: issue.lastGitHubFailureCheckName } : {}),
57
+ ...(snapshot ? { ciSnapshot: snapshot } : {}),
58
+ },
59
+ };
60
+ }
61
+ if (reactiveIntent.runType === "queue_repair") {
62
+ const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
63
+ const incidentContext = parseObjectJson(issue.lastQueueIncidentJson) ?? {};
64
+ const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
65
+ ? failureContext.failureHeadSha
66
+ : undefined;
67
+ if (!hasUnattemptedFailureSignature(issue, fallbackHeadSha))
68
+ return undefined;
69
+ return {
70
+ runType: reactiveIntent.runType,
71
+ wakeReason: reactiveIntent.wakeReason,
72
+ context: {
73
+ ...incidentContext,
74
+ ...failureContext,
75
+ },
76
+ };
77
+ }
78
+ return undefined;
79
+ }
80
+ export class WorkflowWakeResolver {
81
+ issues;
82
+ issueSessions;
83
+ constructor(issues, issueSessions) {
84
+ this.issues = issues;
85
+ this.issueSessions = issueSessions;
86
+ }
87
+ peekIssueWake(projectId, linearIssueId) {
88
+ const explicitWake = this.issueSessions.peekIssueSessionWake(projectId, linearIssueId);
89
+ if (explicitWake)
90
+ return explicitWake;
91
+ const issue = this.issues.getIssue(projectId, linearIssueId);
92
+ if (!issue)
93
+ return undefined;
94
+ const implicitWake = deriveImplicitReactiveWake(issue);
95
+ if (!implicitWake)
96
+ return undefined;
97
+ return {
98
+ eventIds: [],
99
+ runType: implicitWake.runType,
100
+ context: implicitWake.context,
101
+ wakeReason: implicitWake.wakeReason,
102
+ resumeThread: false,
103
+ };
104
+ }
105
+ hasPendingWake(projectId, linearIssueId) {
106
+ return this.peekIssueWake(projectId, linearIssueId) !== undefined;
107
+ }
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchrelay",
3
- "version": "0.69.3",
3
+ "version": "0.69.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {