patchrelay 0.35.6 → 0.35.8

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.35.6",
4
- "commit": "724dfad4eb3b",
5
- "builtAt": "2026-04-03T11:34:35.008Z"
3
+ "version": "0.35.8",
4
+ "commit": "727fa6d1f476",
5
+ "builtAt": "2026-04-03T23:33:50.106Z"
6
6
  }
@@ -0,0 +1,262 @@
1
+ import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
2
+ import { parseGitHubFailureContext } from "./github-failure-context.js";
3
+ import { parseStoredQueueRepairContext } from "./merge-queue-incident.js";
4
+ import { execCommand } from "./utils.js";
5
+ function isDuplicateRepairAttempt(issue, context) {
6
+ const signature = typeof context?.failureSignature === "string" ? context.failureSignature : undefined;
7
+ const headSha = typeof context?.failureHeadSha === "string"
8
+ ? context.failureHeadSha
9
+ : typeof context?.headSha === "string" ? context.headSha : undefined;
10
+ if (!signature)
11
+ return false;
12
+ return issue.lastAttemptedFailureSignature === signature
13
+ && (headSha === undefined || issue.lastAttemptedFailureHeadSha === headSha);
14
+ }
15
+ function buildFailureContext(issue) {
16
+ const storedFailureContext = parseGitHubFailureContext(issue.lastGitHubFailureContextJson);
17
+ const queueRepairContext = issue.lastQueueIncidentJson
18
+ ? parseStoredQueueRepairContext(issue.lastQueueIncidentJson)
19
+ : undefined;
20
+ if (!queueRepairContext
21
+ && !issue.lastGitHubFailureSource
22
+ && !issue.lastGitHubFailureHeadSha
23
+ && !issue.lastGitHubFailureSignature
24
+ && !issue.lastGitHubFailureCheckName
25
+ && !issue.lastGitHubFailureCheckUrl
26
+ && !storedFailureContext) {
27
+ return undefined;
28
+ }
29
+ return {
30
+ ...(issue.lastGitHubFailureSource ? { failureReason: issue.lastGitHubFailureSource } : {}),
31
+ ...(issue.lastGitHubFailureHeadSha ? { failureHeadSha: issue.lastGitHubFailureHeadSha } : {}),
32
+ ...(issue.lastGitHubFailureSignature ? { failureSignature: issue.lastGitHubFailureSignature } : {}),
33
+ ...(issue.lastGitHubFailureCheckName ? { checkName: issue.lastGitHubFailureCheckName } : {}),
34
+ ...(issue.lastGitHubFailureCheckUrl ? { checkUrl: issue.lastGitHubFailureCheckUrl } : {}),
35
+ ...(storedFailureContext ? storedFailureContext : {}),
36
+ ...(queueRepairContext ? queueRepairContext : {}),
37
+ };
38
+ }
39
+ export function resolveBranchOwnerForStateTransition(newState, pendingRunType) {
40
+ if (pendingRunType)
41
+ return "patchrelay";
42
+ if (newState === "awaiting_queue")
43
+ return "merge_steward";
44
+ if (newState === "repairing_ci" || newState === "repairing_queue")
45
+ return "patchrelay";
46
+ return undefined;
47
+ }
48
+ export class IdleIssueReconciler {
49
+ db;
50
+ config;
51
+ deps;
52
+ logger;
53
+ feed;
54
+ constructor(db, config, deps, logger, feed) {
55
+ this.db = db;
56
+ this.config = config;
57
+ this.deps = deps;
58
+ this.logger = logger;
59
+ this.feed = feed;
60
+ }
61
+ async reconcile() {
62
+ for (const issue of this.db.listIdleNonTerminalIssues()) {
63
+ if (issue.prState === "merged") {
64
+ this.advanceIdleIssue(issue, "done", { clearFailureProvenance: true });
65
+ continue;
66
+ }
67
+ if (issue.prReviewState === "approved" && issue.prCheckStatus !== "failed") {
68
+ if (issue.factoryState !== "awaiting_queue" || issue.branchOwner !== "merge_steward") {
69
+ this.advanceIdleIssue(issue, "awaiting_queue", { clearFailureProvenance: true });
70
+ }
71
+ else if (!issue.queueLabelApplied) {
72
+ await this.deps.requestMergeQueueAdmission(issue, issue.projectId);
73
+ }
74
+ continue;
75
+ }
76
+ if (issue.prCheckStatus === "failed") {
77
+ await this.routeFailedIssue(issue);
78
+ continue;
79
+ }
80
+ // Probe GitHub for stale pr_open issues: detect missed reviews,
81
+ // merge conflicts, and other state that webhooks may have missed.
82
+ if (issue.factoryState === "pr_open") {
83
+ await this.reconcileFromGitHub(issue);
84
+ }
85
+ }
86
+ for (const issue of this.db.listBlockedDelegatedIssues()) {
87
+ const unresolved = this.db.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
88
+ if (unresolved === 0) {
89
+ this.db.upsertIssue({
90
+ projectId: issue.projectId,
91
+ linearIssueId: issue.linearIssueId,
92
+ pendingRunType: "implementation",
93
+ });
94
+ this.deps.enqueueIssue(issue.projectId, issue.linearIssueId);
95
+ }
96
+ }
97
+ }
98
+ advanceIdleIssue(issue, newState, options) {
99
+ if (issue.factoryState === newState && !options?.pendingRunType && !options?.clearFailureProvenance) {
100
+ return;
101
+ }
102
+ this.logger.info({ issueKey: issue.issueKey, from: issue.factoryState, to: newState, pendingRunType: options?.pendingRunType }, "Reconciliation: advancing idle issue");
103
+ const resetQueueLabel = newState === "awaiting_queue" || issue.factoryState === "awaiting_queue";
104
+ this.db.upsertIssue({
105
+ projectId: issue.projectId,
106
+ linearIssueId: issue.linearIssueId,
107
+ factoryState: newState,
108
+ ...(options?.pendingRunType ? { pendingRunType: options.pendingRunType } : {}),
109
+ ...(options?.pendingRunType
110
+ ? {
111
+ pendingRunContextJson: options.pendingRunContext ? JSON.stringify(options.pendingRunContext) : null,
112
+ }
113
+ : {}),
114
+ ...(resetQueueLabel ? { queueLabelApplied: false } : {}),
115
+ ...(options?.clearFailureProvenance
116
+ ? {
117
+ lastGitHubFailureSource: null,
118
+ lastGitHubFailureHeadSha: null,
119
+ lastGitHubFailureSignature: null,
120
+ lastGitHubFailureCheckName: null,
121
+ lastGitHubFailureCheckUrl: null,
122
+ lastGitHubFailureContextJson: null,
123
+ lastGitHubFailureAt: null,
124
+ lastQueueIncidentJson: null,
125
+ lastAttemptedFailureHeadSha: null,
126
+ lastAttemptedFailureSignature: null,
127
+ }
128
+ : {}),
129
+ });
130
+ const branchOwner = resolveBranchOwnerForStateTransition(newState, options?.pendingRunType);
131
+ if (branchOwner) {
132
+ this.db.setBranchOwner(issue.projectId, issue.linearIssueId, branchOwner);
133
+ }
134
+ this.feed?.publish({
135
+ level: "info",
136
+ kind: "stage",
137
+ issueKey: issue.issueKey,
138
+ projectId: issue.projectId,
139
+ stage: newState,
140
+ status: "reconciled",
141
+ summary: `Reconciliation: ${issue.factoryState} \u2192 ${newState}`,
142
+ });
143
+ if (newState === "awaiting_queue" && issue.factoryState !== "awaiting_queue") {
144
+ void this.deps.requestMergeQueueAdmission(issue, issue.projectId);
145
+ }
146
+ if (options?.pendingRunType) {
147
+ this.deps.enqueueIssue(issue.projectId, issue.linearIssueId);
148
+ }
149
+ }
150
+ async routeFailedIssue(issue) {
151
+ if (issue.lastGitHubFailureSource === "queue_eviction") {
152
+ const pendingRunContext = buildFailureContext(issue);
153
+ if (isDuplicateRepairAttempt(issue, pendingRunContext)) {
154
+ this.advanceIdleIssue(issue, "repairing_queue");
155
+ }
156
+ else {
157
+ this.advanceIdleIssue(issue, "repairing_queue", {
158
+ pendingRunType: "queue_repair",
159
+ ...(pendingRunContext ? { pendingRunContext } : {}),
160
+ });
161
+ }
162
+ return;
163
+ }
164
+ if (issue.lastGitHubFailureSource === "branch_ci") {
165
+ const pendingRunContext = buildFailureContext(issue);
166
+ if (isDuplicateRepairAttempt(issue, pendingRunContext)) {
167
+ this.advanceIdleIssue(issue, "repairing_ci");
168
+ }
169
+ else {
170
+ this.advanceIdleIssue(issue, "repairing_ci", {
171
+ pendingRunType: "ci_repair",
172
+ ...(pendingRunContext ? { pendingRunContext } : {}),
173
+ });
174
+ }
175
+ return;
176
+ }
177
+ if (issue.factoryState === "awaiting_queue") {
178
+ const inferProject = this.config.projects.find((p) => p.id === issue.projectId);
179
+ const inferProtocol = resolveMergeQueueProtocol(inferProject);
180
+ let inferred = "branch_ci";
181
+ const probeSha = issue.lastGitHubFailureHeadSha ?? issue.lastGitHubCiSnapshotHeadSha;
182
+ if (inferProject?.github?.repoFullName && issue.prNumber && probeSha) {
183
+ try {
184
+ const { stdout } = await execCommand("gh", [
185
+ "api",
186
+ `repos/${inferProject.github.repoFullName}/commits/${probeSha}/check-runs`,
187
+ "--jq", `.check_runs[] | select(.name == "${inferProtocol.evictionCheckName}" and .conclusion == "failure") | .name`,
188
+ ], { timeoutMs: 10_000 });
189
+ if (stdout.trim().length > 0)
190
+ inferred = "queue_eviction";
191
+ }
192
+ catch { /* best effort */ }
193
+ }
194
+ const inferRunType = inferred === "queue_eviction" ? "queue_repair" : "ci_repair";
195
+ const inferState = inferred === "queue_eviction" ? "repairing_queue" : "repairing_ci";
196
+ this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, inferred }, "Inferred failure provenance for awaiting_queue issue");
197
+ const pendingRunContext = buildFailureContext(issue);
198
+ this.advanceIdleIssue(issue, inferState, {
199
+ pendingRunType: inferRunType,
200
+ ...(pendingRunContext ? { pendingRunContext } : {}),
201
+ });
202
+ return;
203
+ }
204
+ const pendingRunContext = buildFailureContext(issue);
205
+ if (isDuplicateRepairAttempt(issue, pendingRunContext)) {
206
+ this.advanceIdleIssue(issue, "repairing_ci");
207
+ }
208
+ else {
209
+ this.advanceIdleIssue(issue, "repairing_ci", {
210
+ pendingRunType: "ci_repair",
211
+ ...(pendingRunContext ? { pendingRunContext } : {}),
212
+ });
213
+ }
214
+ }
215
+ async reconcileFromGitHub(issue) {
216
+ const project = this.config.projects.find((p) => p.id === issue.projectId);
217
+ if (!project?.github?.repoFullName || !issue.prNumber)
218
+ return;
219
+ try {
220
+ const { stdout } = await execCommand("gh", [
221
+ "pr", "view", String(issue.prNumber),
222
+ "--repo", project.github.repoFullName,
223
+ "--json", "state,reviewDecision,mergeable,mergeStateStatus",
224
+ ], { timeoutMs: 10_000 });
225
+ const pr = JSON.parse(stdout);
226
+ if (pr.state === "MERGED") {
227
+ this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prState: "merged" });
228
+ this.advanceIdleIssue(issue, "done", { clearFailureProvenance: true });
229
+ return;
230
+ }
231
+ if (pr.reviewDecision === "APPROVED") {
232
+ this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prReviewState: "approved" });
233
+ this.advanceIdleIssue(issue, "awaiting_queue", { clearFailureProvenance: true });
234
+ return;
235
+ }
236
+ // Merge conflict detected — dispatch a repair run to rebase the branch.
237
+ if (pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY") {
238
+ this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, mergeable: pr.mergeable }, "Reconciliation: PR has merge conflicts, dispatching rebase");
239
+ this.advanceIdleIssue(issue, "repairing_queue", {
240
+ pendingRunType: "queue_repair",
241
+ pendingRunContext: {
242
+ source: "idle_reconciliation",
243
+ failureReason: "merge_conflict_detected",
244
+ failureSignature: `conflict:${issue.prNumber}`,
245
+ },
246
+ });
247
+ this.feed?.publish({
248
+ level: "warn",
249
+ kind: "github",
250
+ issueKey: issue.issueKey,
251
+ projectId: issue.projectId,
252
+ stage: "repairing_queue",
253
+ status: "conflict_detected",
254
+ summary: `PR #${issue.prNumber} has merge conflicts with main, dispatching rebase`,
255
+ });
256
+ }
257
+ }
258
+ catch (error) {
259
+ this.logger.debug({ issueKey: issue.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to query GitHub PR state during reconciliation");
260
+ }
261
+ }
262
+ }
@@ -0,0 +1,143 @@
1
+ import { buildAgentSessionPlanForIssue } from "./agent-session-plan.js";
2
+ import { buildAgentSessionExternalUrls } from "./agent-session-presentation.js";
3
+ const PROGRESS_THROTTLE_MS = 5_000;
4
+ export class LinearSessionSync {
5
+ config;
6
+ db;
7
+ linearProvider;
8
+ logger;
9
+ feed;
10
+ progressThrottle = new Map();
11
+ constructor(config, db, linearProvider, logger, feed) {
12
+ this.config = config;
13
+ this.db = db;
14
+ this.linearProvider = linearProvider;
15
+ this.logger = logger;
16
+ this.feed = feed;
17
+ }
18
+ async emitActivity(issue, content, options) {
19
+ if (!issue.agentSessionId)
20
+ return;
21
+ try {
22
+ const linear = await this.linearProvider.forProject(issue.projectId);
23
+ if (!linear)
24
+ return;
25
+ const allowEphemeral = content.type === "thought" || content.type === "action";
26
+ await linear.createAgentActivity({
27
+ agentSessionId: issue.agentSessionId,
28
+ content,
29
+ ...(options?.ephemeral && allowEphemeral ? { ephemeral: true } : {}),
30
+ });
31
+ }
32
+ catch (error) {
33
+ const msg = error instanceof Error ? error.message : String(error);
34
+ this.logger.warn({ issueKey: issue.issueKey, type: content.type, error: msg }, "Failed to emit Linear activity");
35
+ this.feed?.publish({
36
+ level: "warn",
37
+ kind: "linear",
38
+ issueKey: issue.issueKey,
39
+ projectId: issue.projectId,
40
+ status: "linear_error",
41
+ summary: `Linear activity failed: ${msg}`,
42
+ });
43
+ }
44
+ }
45
+ async syncSession(issue, options) {
46
+ if (!issue.agentSessionId)
47
+ return;
48
+ try {
49
+ const linear = await this.linearProvider.forProject(issue.projectId);
50
+ if (!linear?.updateAgentSession)
51
+ return;
52
+ const externalUrls = buildAgentSessionExternalUrls(this.config, {
53
+ ...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
54
+ ...(issue.prUrl ? { prUrl: issue.prUrl } : {}),
55
+ });
56
+ await linear.updateAgentSession({
57
+ agentSessionId: issue.agentSessionId,
58
+ plan: buildAgentSessionPlanForIssue(issue, options),
59
+ ...(externalUrls ? { externalUrls } : {}),
60
+ });
61
+ }
62
+ catch (error) {
63
+ const msg = error instanceof Error ? error.message : String(error);
64
+ this.logger.warn({ issueKey: issue.issueKey, error: msg }, "Failed to update Linear plan");
65
+ }
66
+ }
67
+ async syncCodexPlan(issue, params) {
68
+ if (!issue.agentSessionId)
69
+ return;
70
+ const plan = params.plan;
71
+ if (!Array.isArray(plan))
72
+ return;
73
+ const STATUS_MAP = {
74
+ pending: "pending",
75
+ inProgress: "inProgress",
76
+ completed: "completed",
77
+ };
78
+ const steps = plan.map((entry) => {
79
+ const e = entry;
80
+ const step = typeof e.step === "string" ? e.step : String(e.step ?? "");
81
+ const status = typeof e.status === "string" ? (STATUS_MAP[e.status] ?? "pending") : "pending";
82
+ return { content: step, status };
83
+ });
84
+ const fullPlan = [
85
+ { content: "Prepare workspace", status: "completed" },
86
+ ...steps,
87
+ { content: "Merge", status: "pending" },
88
+ ];
89
+ try {
90
+ const linear = await this.linearProvider.forProject(issue.projectId);
91
+ if (!linear?.updateAgentSession)
92
+ return;
93
+ await linear.updateAgentSession({
94
+ agentSessionId: issue.agentSessionId,
95
+ plan: fullPlan,
96
+ });
97
+ }
98
+ catch (error) {
99
+ const msg = error instanceof Error ? error.message : String(error);
100
+ this.logger.warn({ issueKey: issue.issueKey, error: msg }, "Failed to sync codex plan to Linear");
101
+ }
102
+ }
103
+ maybeEmitProgress(notification, run) {
104
+ const activity = resolveProgressActivity(notification);
105
+ if (!activity)
106
+ return;
107
+ const now = Date.now();
108
+ const lastEmit = this.progressThrottle.get(run.id) ?? 0;
109
+ if (now - lastEmit < PROGRESS_THROTTLE_MS)
110
+ return;
111
+ this.progressThrottle.set(run.id, now);
112
+ const issue = this.db.getIssue(run.projectId, run.linearIssueId);
113
+ if (issue) {
114
+ void this.emitActivity(issue, activity, { ephemeral: true });
115
+ }
116
+ }
117
+ clearProgress(runId) {
118
+ this.progressThrottle.delete(runId);
119
+ }
120
+ }
121
+ function resolveProgressActivity(notification) {
122
+ if (notification.method === "item/started") {
123
+ const item = notification.params.item;
124
+ if (!item)
125
+ return undefined;
126
+ const type = typeof item.type === "string" ? item.type : undefined;
127
+ if (type === "commandExecution") {
128
+ const cmd = item.command;
129
+ const cmdStr = Array.isArray(cmd) ? cmd.join(" ") : typeof cmd === "string" ? cmd : undefined;
130
+ return { type: "action", action: "Running", parameter: cmdStr?.slice(0, 120) ?? "command" };
131
+ }
132
+ if (type === "mcpToolCall") {
133
+ const server = typeof item.server === "string" ? item.server : "";
134
+ const tool = typeof item.tool === "string" ? item.tool : "";
135
+ return { type: "action", action: "Using", parameter: `${server}/${tool}` };
136
+ }
137
+ if (type === "dynamicToolCall") {
138
+ const tool = typeof item.tool === "string" ? item.tool : "tool";
139
+ return { type: "action", action: "Using", parameter: tool };
140
+ }
141
+ }
142
+ return undefined;
143
+ }
@@ -0,0 +1,131 @@
1
+ import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
2
+ import { execCommand } from "./utils.js";
3
+ const QUEUE_HEALTH_GRACE_MS = 120_000;
4
+ const QUEUE_HEALTH_PROBE_FAILURE_COOLDOWN_MS = 300_000;
5
+ function isDuplicateProbe(issue, context) {
6
+ const signature = typeof context?.failureSignature === "string" ? context.failureSignature : undefined;
7
+ const headSha = typeof context?.failureHeadSha === "string" ? context.failureHeadSha : undefined;
8
+ if (!signature)
9
+ return false;
10
+ return issue.lastAttemptedFailureSignature === signature
11
+ && (headSha === undefined || issue.lastAttemptedFailureHeadSha === headSha);
12
+ }
13
+ export class QueueHealthMonitor {
14
+ db;
15
+ config;
16
+ advancer;
17
+ logger;
18
+ feed;
19
+ probeFailureFeedTimes = new Map();
20
+ constructor(db, config, advancer, logger, feed) {
21
+ this.db = db;
22
+ this.config = config;
23
+ this.advancer = advancer;
24
+ this.logger = logger;
25
+ this.feed = feed;
26
+ }
27
+ async reconcile() {
28
+ for (const issue of this.db.listAwaitingQueueIssues()) {
29
+ await this.probeQueuedIssue(issue);
30
+ }
31
+ }
32
+ async probeQueuedIssue(issue) {
33
+ if (!issue.prNumber)
34
+ return;
35
+ const project = this.config.projects.find((p) => p.id === issue.projectId);
36
+ if (!project?.github?.repoFullName)
37
+ return;
38
+ const age = Date.now() - Date.parse(issue.updatedAt);
39
+ if (age < QUEUE_HEALTH_GRACE_MS)
40
+ return;
41
+ const protocol = resolveMergeQueueProtocol(project);
42
+ let pr;
43
+ try {
44
+ const { stdout } = await execCommand("gh", [
45
+ "pr", "view", String(issue.prNumber),
46
+ "--repo", project.github.repoFullName,
47
+ "--json", "state,mergeable,mergeStateStatus,headRefOid,labels",
48
+ ], { timeoutMs: 10_000 });
49
+ pr = JSON.parse(stdout);
50
+ }
51
+ catch (error) {
52
+ this.logger.debug({ issueKey: issue.issueKey, prNumber: issue.prNumber, error: error instanceof Error ? error.message : String(error) }, "Queue health: failed to probe GitHub PR state");
53
+ const issueKey = `${issue.projectId}::${issue.linearIssueId}`;
54
+ const lastFeedAt = this.probeFailureFeedTimes.get(issueKey) ?? 0;
55
+ if (Date.now() - lastFeedAt >= QUEUE_HEALTH_PROBE_FAILURE_COOLDOWN_MS) {
56
+ this.probeFailureFeedTimes.set(issueKey, Date.now());
57
+ this.feed?.publish({
58
+ level: "info",
59
+ kind: "github",
60
+ issueKey: issue.issueKey,
61
+ projectId: issue.projectId,
62
+ stage: "awaiting_queue",
63
+ status: "queue_health_probe_failed",
64
+ summary: `Queue health: failed to probe PR #${issue.prNumber}`,
65
+ });
66
+ }
67
+ return;
68
+ }
69
+ this.probeFailureFeedTimes.delete(`${issue.projectId}::${issue.linearIssueId}`);
70
+ if (pr.state === "MERGED") {
71
+ this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prState: "merged" });
72
+ this.advancer.advanceIdleIssue(issue, "done", { clearFailureProvenance: true });
73
+ return;
74
+ }
75
+ if (pr.state !== "OPEN")
76
+ return;
77
+ const hasQueueLabel = pr.labels?.some((l) => l.name === protocol.admissionLabel) ?? false;
78
+ if (!hasQueueLabel)
79
+ return;
80
+ const isDirty = pr.mergeStateStatus === "DIRTY" || pr.mergeable === "CONFLICTING";
81
+ let hasEvictionCheckRun = false;
82
+ if (!isDirty) {
83
+ try {
84
+ const { stdout: checksOut } = await execCommand("gh", [
85
+ "api", `repos/${project.github.repoFullName}/commits/${pr.headRefOid}/check-runs`,
86
+ "--jq", `.check_runs[] | select(.name == "${protocol.evictionCheckName}" and .conclusion == "failure") | .name`,
87
+ ], { timeoutMs: 10_000 });
88
+ hasEvictionCheckRun = checksOut.trim().length > 0;
89
+ }
90
+ catch {
91
+ // Best-effort check.
92
+ }
93
+ }
94
+ if (isDirty || hasEvictionCheckRun) {
95
+ const headRefOid = pr.headRefOid ?? "unknown";
96
+ const reason = hasEvictionCheckRun ? "queue_eviction_missed" : "preemptive_conflict";
97
+ const signature = `preemptive_queue_conflict:${headRefOid}`;
98
+ const pendingRunContext = {
99
+ source: "queue_health_monitor",
100
+ failureReason: reason,
101
+ failureHeadSha: headRefOid,
102
+ failureSignature: signature,
103
+ };
104
+ if (isDuplicateProbe(issue, pendingRunContext)) {
105
+ return;
106
+ }
107
+ this.db.upsertIssue({
108
+ projectId: issue.projectId,
109
+ linearIssueId: issue.linearIssueId,
110
+ lastAttemptedFailureHeadSha: headRefOid,
111
+ lastAttemptedFailureSignature: signature,
112
+ });
113
+ this.advancer.advanceIdleIssue(issue, "repairing_queue", {
114
+ pendingRunType: "queue_repair",
115
+ pendingRunContext,
116
+ });
117
+ this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, headRefOid, reason }, "Queue health: queue issue detected, dispatching repair");
118
+ this.feed?.publish({
119
+ level: "warn",
120
+ kind: "github",
121
+ issueKey: issue.issueKey,
122
+ projectId: issue.projectId,
123
+ stage: "repairing_queue",
124
+ status: hasEvictionCheckRun ? "queue_health_eviction_detected" : "queue_health_conflict_detected",
125
+ summary: hasEvictionCheckRun
126
+ ? `Queue health: missed eviction detected on PR #${issue.prNumber}, dispatching repair`
127
+ : `Queue health: merge conflict detected on PR #${issue.prNumber}, dispatching preemptive repair`,
128
+ });
129
+ }
130
+ }
131
+ }