patchrelay 0.36.7 → 0.36.9
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.
- package/dist/build-info.json +3 -3
- package/dist/cli/cluster-health.js +2 -2
- package/dist/cli/data.js +20 -20
- package/dist/db/issue-session-store.js +284 -0
- package/dist/db/issue-store.js +559 -0
- package/dist/db/run-store.js +125 -0
- package/dist/db/webhook-event-store.js +71 -0
- package/dist/db.js +52 -1138
- package/dist/github-webhook-handler.js +44 -44
- package/dist/idle-reconciliation.js +20 -20
- package/dist/interrupted-run-recovery.js +176 -0
- package/dist/issue-query-service.js +13 -13
- package/dist/issue-session-lease-service.js +143 -0
- package/dist/issue-session-projector.js +114 -0
- package/dist/linear-session-sync.js +10 -10
- package/dist/queue-health-monitor.js +5 -5
- package/dist/run-completion-policy.js +412 -0
- package/dist/run-finalizer.js +172 -0
- package/dist/run-launcher.js +193 -0
- package/dist/run-orchestrator.js +145 -1505
- package/dist/run-recovery-service.js +209 -0
- package/dist/run-wake-planner.js +101 -0
- package/dist/service.js +33 -33
- package/dist/tracked-issue-projector.js +69 -0
- package/dist/webhook-handler.js +64 -693
- package/dist/webhooks/agent-session-handler.js +212 -0
- package/dist/webhooks/comment-policy.js +41 -0
- package/dist/webhooks/comment-wake-handler.js +133 -0
- package/dist/webhooks/decision-helpers.js +74 -0
- package/dist/webhooks/desired-stage-recorder.js +177 -0
- package/dist/webhooks/issue-removal-handler.js +68 -0
- package/dist/worktree-manager.js +69 -0
- package/package.json +1 -1
|
@@ -60,11 +60,11 @@ export class GitHubWebhookHandler {
|
|
|
60
60
|
}
|
|
61
61
|
async acceptGitHubWebhook(params) {
|
|
62
62
|
// Deduplicate
|
|
63
|
-
if (this.db.isWebhookDuplicate(params.deliveryId)) {
|
|
63
|
+
if (this.db.webhookEvents.isWebhookDuplicate(params.deliveryId)) {
|
|
64
64
|
return { status: 200, body: { ok: true, duplicate: true } };
|
|
65
65
|
}
|
|
66
66
|
// Store the event
|
|
67
|
-
const stored = this.db.insertWebhookEvent(params.deliveryId, new Date().toISOString());
|
|
67
|
+
const stored = this.db.webhookEvents.insertWebhookEvent(params.deliveryId, new Date().toISOString());
|
|
68
68
|
// Parse payload
|
|
69
69
|
const payload = safeJsonParse(params.rawBody.toString("utf8"));
|
|
70
70
|
if (!payload) {
|
|
@@ -130,14 +130,14 @@ export class GitHubWebhookHandler {
|
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
132
|
// Route to issue via branch name
|
|
133
|
-
const issue = this.db.getIssueByBranch(event.branchName);
|
|
133
|
+
const issue = this.db.issues.getIssueByBranch(event.branchName);
|
|
134
134
|
if (!issue) {
|
|
135
135
|
this.logger.debug({ branchName: event.branchName, triggerEvent: event.triggerEvent }, "GitHub webhook: no matching issue for branch");
|
|
136
136
|
return;
|
|
137
137
|
}
|
|
138
138
|
const project = this.config.projects.find((p) => p.id === issue.projectId);
|
|
139
139
|
// Update PR state on the issue
|
|
140
|
-
this.db.upsertIssue({
|
|
140
|
+
this.db.issues.upsertIssue({
|
|
141
141
|
projectId: issue.projectId,
|
|
142
142
|
linearIssueId: issue.linearIssueId,
|
|
143
143
|
...(event.prNumber !== undefined ? { prNumber: event.prNumber } : {}),
|
|
@@ -158,28 +158,28 @@ export class GitHubWebhookHandler {
|
|
|
158
158
|
const queueEvictionCheck = this.isQueueEvictionFailure(issue, event, project);
|
|
159
159
|
if (!isMetadataOnlyCheckEvent(event) || queueEvictionCheck) {
|
|
160
160
|
// Re-read issue after PR metadata upsert so guards see fresh prReviewState
|
|
161
|
-
const afterMetadata = this.db.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
161
|
+
const afterMetadata = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
162
162
|
const newState = this.resolveFactoryStateForEvent(afterMetadata, event, project);
|
|
163
163
|
// Only transition and notify when the state actually changes.
|
|
164
164
|
// Multiple check_suite events can arrive for the same outcome.
|
|
165
165
|
if (newState && newState !== afterMetadata.factoryState) {
|
|
166
|
-
this.db.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
166
|
+
this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
167
167
|
projectId: issue.projectId,
|
|
168
168
|
linearIssueId: issue.linearIssueId,
|
|
169
169
|
factoryState: newState,
|
|
170
170
|
});
|
|
171
171
|
this.logger.info({ issueKey: issue.issueKey, from: afterMetadata.factoryState, to: newState, trigger: event.triggerEvent }, "Factory state transition from GitHub event");
|
|
172
|
-
const transitionedIssue = this.db.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
172
|
+
const transitionedIssue = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
173
173
|
void this.emitLinearActivity(transitionedIssue, newState, event);
|
|
174
174
|
void this.syncLinearSession(transitionedIssue);
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
// Re-read issue after all upserts so reactive run logic sees current state
|
|
178
|
-
const freshIssue = this.db.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
178
|
+
const freshIssue = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
179
179
|
// Reset repair counters on new push — but only when no repair run is active,
|
|
180
180
|
// since Codex pushes during repair and resetting mid-run would bypass budgets.
|
|
181
181
|
if (event.triggerEvent === "pr_synchronize" && !freshIssue.activeRunId) {
|
|
182
|
-
this.db.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
182
|
+
this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
183
183
|
projectId: issue.projectId,
|
|
184
184
|
linearIssueId: issue.linearIssueId,
|
|
185
185
|
ciRepairAttempts: 0,
|
|
@@ -240,7 +240,7 @@ export class GitHubWebhookHandler {
|
|
|
240
240
|
}
|
|
241
241
|
async updateCiSnapshot(issue, event, project) {
|
|
242
242
|
if (event.triggerEvent === "pr_merged") {
|
|
243
|
-
this.db.upsertIssue({
|
|
243
|
+
this.db.issues.upsertIssue({
|
|
244
244
|
projectId: issue.projectId,
|
|
245
245
|
linearIssueId: issue.linearIssueId,
|
|
246
246
|
lastGitHubCiSnapshotHeadSha: null,
|
|
@@ -252,7 +252,7 @@ export class GitHubWebhookHandler {
|
|
|
252
252
|
return;
|
|
253
253
|
}
|
|
254
254
|
if (event.triggerEvent === "pr_synchronize") {
|
|
255
|
-
this.db.upsertIssue({
|
|
255
|
+
this.db.issues.upsertIssue({
|
|
256
256
|
projectId: issue.projectId,
|
|
257
257
|
linearIssueId: issue.linearIssueId,
|
|
258
258
|
lastGitHubCiSnapshotHeadSha: event.headSha ?? null,
|
|
@@ -279,7 +279,7 @@ export class GitHubWebhookHandler {
|
|
|
279
279
|
gateCheckNames: this.getGateCheckNames(project),
|
|
280
280
|
});
|
|
281
281
|
if (!snapshot) {
|
|
282
|
-
this.db.upsertIssue({
|
|
282
|
+
this.db.issues.upsertIssue({
|
|
283
283
|
projectId: issue.projectId,
|
|
284
284
|
linearIssueId: issue.linearIssueId,
|
|
285
285
|
lastGitHubCiSnapshotHeadSha: event.headSha ?? issue.lastGitHubCiSnapshotHeadSha ?? null,
|
|
@@ -300,7 +300,7 @@ export class GitHubWebhookHandler {
|
|
|
300
300
|
});
|
|
301
301
|
return;
|
|
302
302
|
}
|
|
303
|
-
this.db.upsertIssue({
|
|
303
|
+
this.db.issues.upsertIssue({
|
|
304
304
|
projectId: issue.projectId,
|
|
305
305
|
linearIssueId: issue.linearIssueId,
|
|
306
306
|
prCheckStatus: snapshot.gateCheckStatus,
|
|
@@ -340,8 +340,8 @@ export class GitHubWebhookHandler {
|
|
|
340
340
|
if (this.hasDuplicatePendingReactiveRun(issue, "queue_repair", failureContext)) {
|
|
341
341
|
return;
|
|
342
342
|
}
|
|
343
|
-
const hadPendingWake = this.db.hasPendingIssueSessionEvents(issue.projectId, issue.linearIssueId);
|
|
344
|
-
this.db.upsertIssue({
|
|
343
|
+
const hadPendingWake = this.db.issueSessions.hasPendingIssueSessionEvents(issue.projectId, issue.linearIssueId);
|
|
344
|
+
this.db.issues.upsertIssue({
|
|
345
345
|
projectId: issue.projectId,
|
|
346
346
|
linearIssueId: issue.linearIssueId,
|
|
347
347
|
lastGitHubFailureSource: "queue_eviction",
|
|
@@ -354,7 +354,7 @@ export class GitHubWebhookHandler {
|
|
|
354
354
|
lastQueueSignalAt: new Date().toISOString(),
|
|
355
355
|
lastQueueIncidentJson: JSON.stringify(queueRepairContext),
|
|
356
356
|
});
|
|
357
|
-
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
357
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
358
358
|
projectId: issue.projectId,
|
|
359
359
|
linearIssueId: issue.linearIssueId,
|
|
360
360
|
eventType: "merge_steward_incident",
|
|
@@ -364,7 +364,7 @@ export class GitHubWebhookHandler {
|
|
|
364
364
|
}),
|
|
365
365
|
dedupeKey: failureContext.failureSignature,
|
|
366
366
|
});
|
|
367
|
-
this.db.setBranchOwnerRespectingActiveLease(issue.projectId, issue.linearIssueId, "patchrelay");
|
|
367
|
+
this.db.issueSessions.setBranchOwnerRespectingActiveLease(issue.projectId, issue.linearIssueId, "patchrelay");
|
|
368
368
|
const queuedRunType = hadPendingWake
|
|
369
369
|
? this.peekPendingSessionWakeRunType(issue.projectId, issue.linearIssueId)
|
|
370
370
|
: this.enqueuePendingSessionWake(issue.projectId, issue.linearIssueId);
|
|
@@ -397,9 +397,9 @@ export class GitHubWebhookHandler {
|
|
|
397
397
|
if (this.hasDuplicatePendingReactiveRun(issue, "ci_repair", failureContext)) {
|
|
398
398
|
return;
|
|
399
399
|
}
|
|
400
|
-
const hadPendingWake = this.db.hasPendingIssueSessionEvents(issue.projectId, issue.linearIssueId);
|
|
400
|
+
const hadPendingWake = this.db.issueSessions.hasPendingIssueSessionEvents(issue.projectId, issue.linearIssueId);
|
|
401
401
|
const snapshot = this.getRelevantCiSnapshot(issue, event);
|
|
402
|
-
this.db.upsertIssue({
|
|
402
|
+
this.db.issues.upsertIssue({
|
|
403
403
|
projectId: issue.projectId,
|
|
404
404
|
linearIssueId: issue.linearIssueId,
|
|
405
405
|
lastGitHubFailureSource: "branch_ci",
|
|
@@ -411,7 +411,7 @@ export class GitHubWebhookHandler {
|
|
|
411
411
|
lastGitHubFailureAt: new Date().toISOString(),
|
|
412
412
|
lastQueueIncidentJson: null,
|
|
413
413
|
});
|
|
414
|
-
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
414
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
415
415
|
projectId: issue.projectId,
|
|
416
416
|
linearIssueId: issue.linearIssueId,
|
|
417
417
|
eventType: "settled_red_ci",
|
|
@@ -422,7 +422,7 @@ export class GitHubWebhookHandler {
|
|
|
422
422
|
}),
|
|
423
423
|
dedupeKey: failureContext.failureSignature,
|
|
424
424
|
});
|
|
425
|
-
this.db.setBranchOwnerRespectingActiveLease(issue.projectId, issue.linearIssueId, "patchrelay");
|
|
425
|
+
this.db.issueSessions.setBranchOwnerRespectingActiveLease(issue.projectId, issue.linearIssueId, "patchrelay");
|
|
426
426
|
const queuedRunType = hadPendingWake
|
|
427
427
|
? this.peekPendingSessionWakeRunType(issue.projectId, issue.linearIssueId)
|
|
428
428
|
: this.enqueuePendingSessionWake(issue.projectId, issue.linearIssueId);
|
|
@@ -440,7 +440,7 @@ export class GitHubWebhookHandler {
|
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
442
|
if (event.triggerEvent === "review_changes_requested") {
|
|
443
|
-
const hadPendingWake = this.db.hasPendingIssueSessionEvents(issue.projectId, issue.linearIssueId);
|
|
443
|
+
const hadPendingWake = this.db.issueSessions.hasPendingIssueSessionEvents(issue.projectId, issue.linearIssueId);
|
|
444
444
|
const reviewComments = await this.fetchReviewCommentsForEvent(event).catch((error) => {
|
|
445
445
|
this.logger.warn({
|
|
446
446
|
issueKey: issue.issueKey,
|
|
@@ -450,7 +450,7 @@ export class GitHubWebhookHandler {
|
|
|
450
450
|
}, "Failed to fetch inline review comments for requested-changes event");
|
|
451
451
|
return undefined;
|
|
452
452
|
});
|
|
453
|
-
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
453
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
454
454
|
projectId: issue.projectId,
|
|
455
455
|
linearIssueId: issue.linearIssueId,
|
|
456
456
|
eventType: "review_changes_requested",
|
|
@@ -468,7 +468,7 @@ export class GitHubWebhookHandler {
|
|
|
468
468
|
event.reviewerName ?? "unknown-reviewer",
|
|
469
469
|
].join("::"),
|
|
470
470
|
});
|
|
471
|
-
this.db.setBranchOwnerRespectingActiveLease(issue.projectId, issue.linearIssueId, "patchrelay");
|
|
471
|
+
this.db.issueSessions.setBranchOwnerRespectingActiveLease(issue.projectId, issue.linearIssueId, "patchrelay");
|
|
472
472
|
const queuedRunType = hadPendingWake
|
|
473
473
|
? this.peekPendingSessionWakeRunType(issue.projectId, issue.linearIssueId)
|
|
474
474
|
: this.enqueuePendingSessionWake(issue.projectId, issue.linearIssueId);
|
|
@@ -489,14 +489,14 @@ export class GitHubWebhookHandler {
|
|
|
489
489
|
}
|
|
490
490
|
async handleTerminalPrEvent(issue, event) {
|
|
491
491
|
const eventType = event.triggerEvent === "pr_merged" ? "pr_merged" : "pr_closed";
|
|
492
|
-
this.db.appendIssueSessionEvent({
|
|
492
|
+
this.db.issueSessions.appendIssueSessionEvent({
|
|
493
493
|
projectId: issue.projectId,
|
|
494
494
|
linearIssueId: issue.linearIssueId,
|
|
495
495
|
eventType,
|
|
496
496
|
dedupeKey: [eventType, issue.prNumber ?? event.prNumber ?? "unknown-pr", issue.prHeadSha ?? event.headSha ?? "unknown-sha"].join("::"),
|
|
497
497
|
});
|
|
498
|
-
this.db.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
499
|
-
const run = issue.activeRunId ? this.db.
|
|
498
|
+
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
499
|
+
const run = issue.activeRunId ? this.db.runs.getRunById(issue.activeRunId) : undefined;
|
|
500
500
|
if (run?.threadId && run.turnId) {
|
|
501
501
|
try {
|
|
502
502
|
await this.codex.steerTurn({
|
|
@@ -513,29 +513,29 @@ export class GitHubWebhookHandler {
|
|
|
513
513
|
}
|
|
514
514
|
const commitTerminalUpdate = () => {
|
|
515
515
|
if (run) {
|
|
516
|
-
this.db.finishRun(run.id, {
|
|
516
|
+
this.db.runs.finishRun(run.id, {
|
|
517
517
|
status: "released",
|
|
518
518
|
failureReason: event.triggerEvent === "pr_merged"
|
|
519
519
|
? "Pull request merged during active run"
|
|
520
520
|
: "Pull request closed during active run",
|
|
521
521
|
});
|
|
522
522
|
}
|
|
523
|
-
this.db.upsertIssue({
|
|
523
|
+
this.db.issues.upsertIssue({
|
|
524
524
|
projectId: issue.projectId,
|
|
525
525
|
linearIssueId: issue.linearIssueId,
|
|
526
526
|
activeRunId: null,
|
|
527
527
|
factoryState: event.triggerEvent === "pr_merged" ? "done" : "failed",
|
|
528
528
|
});
|
|
529
529
|
};
|
|
530
|
-
const activeLease = this.db.getActiveIssueSessionLease(issue.projectId, issue.linearIssueId);
|
|
530
|
+
const activeLease = this.db.issueSessions.getActiveIssueSessionLease(issue.projectId, issue.linearIssueId);
|
|
531
531
|
if (activeLease) {
|
|
532
|
-
this.db.withIssueSessionLease(issue.projectId, issue.linearIssueId, activeLease.leaseId, commitTerminalUpdate);
|
|
532
|
+
this.db.issueSessions.withIssueSessionLease(issue.projectId, issue.linearIssueId, activeLease.leaseId, commitTerminalUpdate);
|
|
533
533
|
}
|
|
534
534
|
else {
|
|
535
535
|
this.db.transaction(commitTerminalUpdate);
|
|
536
536
|
}
|
|
537
|
-
this.db.releaseIssueSessionLeaseRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
538
|
-
const updatedIssue = this.db.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
537
|
+
this.db.issueSessions.releaseIssueSessionLeaseRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
538
|
+
const updatedIssue = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
539
539
|
if (event.triggerEvent === "pr_merged") {
|
|
540
540
|
await this.completeLinearIssueAfterMerge(updatedIssue);
|
|
541
541
|
}
|
|
@@ -554,7 +554,7 @@ export class GitHubWebhookHandler {
|
|
|
554
554
|
}
|
|
555
555
|
const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
|
|
556
556
|
if (normalizedCurrent === targetState.trim().toLowerCase()) {
|
|
557
|
-
this.db.upsertIssue({
|
|
557
|
+
this.db.issues.upsertIssue({
|
|
558
558
|
projectId: issue.projectId,
|
|
559
559
|
linearIssueId: issue.linearIssueId,
|
|
560
560
|
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
@@ -563,7 +563,7 @@ export class GitHubWebhookHandler {
|
|
|
563
563
|
return;
|
|
564
564
|
}
|
|
565
565
|
const updated = await linear.setIssueState(issue.linearIssueId, targetState);
|
|
566
|
-
this.db.upsertIssue({
|
|
566
|
+
this.db.issues.upsertIssue({
|
|
567
567
|
projectId: issue.projectId,
|
|
568
568
|
linearIssueId: issue.linearIssueId,
|
|
569
569
|
...(updated.stateName ? { currentLinearState: updated.stateName } : {}),
|
|
@@ -587,7 +587,7 @@ export class GitHubWebhookHandler {
|
|
|
587
587
|
const failureContext = source === "queue_eviction"
|
|
588
588
|
? this.buildQueueFailureContext(issue, event)
|
|
589
589
|
: await this.resolveBranchFailureContext(issue, event, project);
|
|
590
|
-
this.db.upsertIssue({
|
|
590
|
+
this.db.issues.upsertIssue({
|
|
591
591
|
projectId: issue.projectId,
|
|
592
592
|
linearIssueId: issue.linearIssueId,
|
|
593
593
|
lastGitHubFailureSource: source,
|
|
@@ -614,7 +614,7 @@ export class GitHubWebhookHandler {
|
|
|
614
614
|
if (event.triggerEvent === "check_passed" && !this.canClearFailureProvenance(issue, event, project)) {
|
|
615
615
|
return;
|
|
616
616
|
}
|
|
617
|
-
this.db.upsertIssue({
|
|
617
|
+
this.db.issues.upsertIssue({
|
|
618
618
|
projectId: issue.projectId,
|
|
619
619
|
linearIssueId: issue.linearIssueId,
|
|
620
620
|
lastGitHubFailureSource: null,
|
|
@@ -685,7 +685,7 @@ export class GitHubWebhookHandler {
|
|
|
685
685
|
: typeof failureContext.headSha === "string" ? failureContext.headSha : undefined;
|
|
686
686
|
if (!signature)
|
|
687
687
|
return false;
|
|
688
|
-
const pendingWake = this.db.peekIssueSessionWake(issue.projectId, issue.linearIssueId);
|
|
688
|
+
const pendingWake = this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId);
|
|
689
689
|
if (pendingWake?.runType === runType) {
|
|
690
690
|
const existing = pendingWake.context;
|
|
691
691
|
if (existing?.failureSignature === signature
|
|
@@ -763,7 +763,7 @@ export class GitHubWebhookHandler {
|
|
|
763
763
|
return !issue.lastGitHubFailureHeadSha || issue.lastGitHubFailureHeadSha === event.headSha;
|
|
764
764
|
}
|
|
765
765
|
getRelevantCiSnapshot(issue, event) {
|
|
766
|
-
const snapshot = this.db.getLatestGitHubCiSnapshot(issue.projectId, issue.linearIssueId);
|
|
766
|
+
const snapshot = this.db.issues.getLatestGitHubCiSnapshot(issue.projectId, issue.linearIssueId);
|
|
767
767
|
if (!snapshot)
|
|
768
768
|
return undefined;
|
|
769
769
|
if (snapshot.headSha !== event.headSha)
|
|
@@ -904,7 +904,7 @@ export class GitHubWebhookHandler {
|
|
|
904
904
|
const prNumber = typeof issuePayload.number === "number" ? issuePayload.number : undefined;
|
|
905
905
|
if (!prNumber)
|
|
906
906
|
return;
|
|
907
|
-
const issue = this.db.getIssueByPrNumber(prNumber);
|
|
907
|
+
const issue = this.db.issues.getIssueByPrNumber(prNumber);
|
|
908
908
|
if (!issue)
|
|
909
909
|
return;
|
|
910
910
|
if (!this.isPatchRelayOwnedPr(issue))
|
|
@@ -920,7 +920,7 @@ export class GitHubWebhookHandler {
|
|
|
920
920
|
detail: body.slice(0, 200),
|
|
921
921
|
});
|
|
922
922
|
if (issue.activeRunId) {
|
|
923
|
-
const run = this.db.
|
|
923
|
+
const run = this.db.runs.getRunById(issue.activeRunId);
|
|
924
924
|
if (run?.threadId && run.turnId) {
|
|
925
925
|
try {
|
|
926
926
|
await this.codex.steerTurn({
|
|
@@ -937,7 +937,7 @@ export class GitHubWebhookHandler {
|
|
|
937
937
|
}
|
|
938
938
|
}
|
|
939
939
|
}
|
|
940
|
-
this.db.appendIssueSessionEvent({
|
|
940
|
+
this.db.issueSessions.appendIssueSessionEvent({
|
|
941
941
|
projectId: issue.projectId,
|
|
942
942
|
linearIssueId: issue.linearIssueId,
|
|
943
943
|
eventType: "followup_comment",
|
|
@@ -961,10 +961,10 @@ export class GitHubWebhookHandler {
|
|
|
961
961
|
return response.statusText || `GitHub API responded with ${response.status}`;
|
|
962
962
|
}
|
|
963
963
|
peekPendingSessionWakeRunType(projectId, issueId) {
|
|
964
|
-
return this.db.peekIssueSessionWake(projectId, issueId)?.runType;
|
|
964
|
+
return this.db.issueSessions.peekIssueSessionWake(projectId, issueId)?.runType;
|
|
965
965
|
}
|
|
966
966
|
enqueuePendingSessionWake(projectId, issueId) {
|
|
967
|
-
const wake = this.db.peekIssueSessionWake(projectId, issueId);
|
|
967
|
+
const wake = this.db.issueSessions.peekIssueSessionWake(projectId, issueId);
|
|
968
968
|
if (!wake) {
|
|
969
969
|
return undefined;
|
|
970
970
|
}
|
|
@@ -110,7 +110,7 @@ export class IdleIssueReconciler {
|
|
|
110
110
|
this.feed = feed;
|
|
111
111
|
}
|
|
112
112
|
async reconcile() {
|
|
113
|
-
for (const issue of this.db.listIdleNonTerminalIssues()) {
|
|
113
|
+
for (const issue of this.db.issues.listIdleNonTerminalIssues()) {
|
|
114
114
|
if (issue.prState === "merged") {
|
|
115
115
|
this.advanceIdleIssue(issue, "done", { clearFailureProvenance: true });
|
|
116
116
|
continue;
|
|
@@ -145,21 +145,21 @@ export class IdleIssueReconciler {
|
|
|
145
145
|
await this.reconcileFromGitHub(issue);
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
for (const issue of this.db.listIssues()) {
|
|
148
|
+
for (const issue of this.db.issues.listIssues()) {
|
|
149
149
|
if (!this.shouldProbeTerminalIssueFromGitHub(issue))
|
|
150
150
|
continue;
|
|
151
151
|
await this.reconcileFromGitHub(issue);
|
|
152
152
|
}
|
|
153
|
-
for (const issue of this.db.listBlockedDelegatedIssues()) {
|
|
154
|
-
const unresolved = this.db.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
153
|
+
for (const issue of this.db.issues.listBlockedDelegatedIssues()) {
|
|
154
|
+
const unresolved = this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
155
155
|
if (unresolved === 0) {
|
|
156
|
-
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
156
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
157
157
|
projectId: issue.projectId,
|
|
158
158
|
linearIssueId: issue.linearIssueId,
|
|
159
159
|
eventType: "delegated",
|
|
160
160
|
dedupeKey: `delegated:${issue.linearIssueId}`,
|
|
161
161
|
});
|
|
162
|
-
if (this.db.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
162
|
+
if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
163
163
|
this.deps.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
164
164
|
}
|
|
165
165
|
}
|
|
@@ -179,7 +179,7 @@ export class IdleIssueReconciler {
|
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
181
|
this.logger.info({ issueKey: issue.issueKey, from: issue.factoryState, to: newState, pendingRunType: options?.pendingRunType }, "Reconciliation: advancing idle issue");
|
|
182
|
-
this.db.upsertIssue({
|
|
182
|
+
this.db.issues.upsertIssue({
|
|
183
183
|
projectId: issue.projectId,
|
|
184
184
|
linearIssueId: issue.linearIssueId,
|
|
185
185
|
factoryState: newState,
|
|
@@ -206,7 +206,7 @@ export class IdleIssueReconciler {
|
|
|
206
206
|
});
|
|
207
207
|
const branchOwner = resolveBranchOwnerForStateTransition(newState, options?.pendingRunType);
|
|
208
208
|
if (branchOwner) {
|
|
209
|
-
this.db.setBranchOwner(issue.projectId, issue.linearIssueId, branchOwner);
|
|
209
|
+
this.db.issues.setBranchOwner(issue.projectId, issue.linearIssueId, branchOwner);
|
|
210
210
|
}
|
|
211
211
|
if (options?.pendingRunType) {
|
|
212
212
|
this.appendWakeEvent(issue, options.pendingRunType, options.pendingRunContext, "idle_reconciliation");
|
|
@@ -220,7 +220,7 @@ export class IdleIssueReconciler {
|
|
|
220
220
|
status: "reconciled",
|
|
221
221
|
summary: `Reconciliation: ${issue.factoryState} \u2192 ${newState}`,
|
|
222
222
|
});
|
|
223
|
-
if (options?.pendingRunType && this.db.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
223
|
+
if (options?.pendingRunType && this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
224
224
|
this.deps.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
225
225
|
}
|
|
226
226
|
}
|
|
@@ -243,7 +243,7 @@ export class IdleIssueReconciler {
|
|
|
243
243
|
eventType = "delegated";
|
|
244
244
|
dedupeKey = `${dedupeScope}:implementation:${issue.linearIssueId}`;
|
|
245
245
|
}
|
|
246
|
-
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
246
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
247
247
|
projectId: issue.projectId,
|
|
248
248
|
linearIssueId: issue.linearIssueId,
|
|
249
249
|
eventType,
|
|
@@ -254,7 +254,7 @@ export class IdleIssueReconciler {
|
|
|
254
254
|
async routeFailedIssue(issue) {
|
|
255
255
|
issue = await this.refreshMissingFailureProvenance(issue);
|
|
256
256
|
issue = await this.reclassifyStaleBranchFailure(issue);
|
|
257
|
-
const latestRun = this.db.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
257
|
+
const latestRun = this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
258
258
|
const ignoreDuplicateAttempt = latestRun?.status === "failed"
|
|
259
259
|
&& latestRun.failureReason === "Codex turn was interrupted";
|
|
260
260
|
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
@@ -309,7 +309,7 @@ export class IdleIssueReconciler {
|
|
|
309
309
|
?? (inferred === "queue_eviction" && failureHeadSha && checkName
|
|
310
310
|
? ["queue_eviction", failureHeadSha, checkName].join("::")
|
|
311
311
|
: null);
|
|
312
|
-
this.db.upsertIssue({
|
|
312
|
+
this.db.issues.upsertIssue({
|
|
313
313
|
projectId: issue.projectId,
|
|
314
314
|
linearIssueId: issue.linearIssueId,
|
|
315
315
|
lastGitHubFailureSource: inferred,
|
|
@@ -317,7 +317,7 @@ export class IdleIssueReconciler {
|
|
|
317
317
|
...(checkName ? { lastGitHubFailureCheckName: checkName } : {}),
|
|
318
318
|
...(failureSignature ? { lastGitHubFailureSignature: failureSignature } : {}),
|
|
319
319
|
});
|
|
320
|
-
const refreshed = this.db.getIssue(issue.projectId, issue.linearIssueId);
|
|
320
|
+
const refreshed = this.db.issues.getIssue(issue.projectId, issue.linearIssueId);
|
|
321
321
|
if (!refreshed)
|
|
322
322
|
return issue;
|
|
323
323
|
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, inferred, factoryState: issue.factoryState }, "Recovered missing failure provenance from GitHub state");
|
|
@@ -337,7 +337,7 @@ export class IdleIssueReconciler {
|
|
|
337
337
|
const checkName = issue.lastGitHubFailureCheckName ?? protocol.evictionCheckName;
|
|
338
338
|
const failureSignature = issue.lastGitHubFailureSignature
|
|
339
339
|
?? (failureHeadSha && checkName ? ["queue_eviction", failureHeadSha, checkName].join("::") : null);
|
|
340
|
-
this.db.upsertIssue({
|
|
340
|
+
this.db.issues.upsertIssue({
|
|
341
341
|
projectId: issue.projectId,
|
|
342
342
|
linearIssueId: issue.linearIssueId,
|
|
343
343
|
lastGitHubFailureSource: "queue_eviction",
|
|
@@ -345,7 +345,7 @@ export class IdleIssueReconciler {
|
|
|
345
345
|
...(checkName ? { lastGitHubFailureCheckName: checkName } : {}),
|
|
346
346
|
...(failureSignature ? { lastGitHubFailureSignature: failureSignature } : {}),
|
|
347
347
|
});
|
|
348
|
-
const refreshed = this.db.getIssue(issue.projectId, issue.linearIssueId);
|
|
348
|
+
const refreshed = this.db.issues.getIssue(issue.projectId, issue.linearIssueId);
|
|
349
349
|
if (!refreshed)
|
|
350
350
|
return issue;
|
|
351
351
|
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reclassified stale branch failure as queue repair from GitHub state");
|
|
@@ -410,7 +410,7 @@ export class IdleIssueReconciler {
|
|
|
410
410
|
const previousHeadSha = issue.prHeadSha;
|
|
411
411
|
const gateCheckNames = getGateCheckNames(project);
|
|
412
412
|
const gateCheckStatus = deriveGateCheckStatusFromRollup(pr.statusCheckRollup, gateCheckNames);
|
|
413
|
-
this.db.upsertIssue({
|
|
413
|
+
this.db.issues.upsertIssue({
|
|
414
414
|
projectId: issue.projectId,
|
|
415
415
|
linearIssueId: issue.linearIssueId,
|
|
416
416
|
...(pr.headRefOid ? { prHeadSha: pr.headRefOid } : {}),
|
|
@@ -433,13 +433,13 @@ export class IdleIssueReconciler {
|
|
|
433
433
|
: {}),
|
|
434
434
|
});
|
|
435
435
|
if (pr.state === "MERGED") {
|
|
436
|
-
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prState: "merged" });
|
|
436
|
+
this.db.issues.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prState: "merged" });
|
|
437
437
|
this.advanceIdleIssue(issue, "done", { clearFailureProvenance: true });
|
|
438
438
|
return;
|
|
439
439
|
}
|
|
440
440
|
if (pr.state === "CLOSED") {
|
|
441
441
|
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber }, "Reconciliation: PR was closed, re-delegating for implementation");
|
|
442
|
-
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prState: "closed" });
|
|
442
|
+
this.db.issues.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prState: "closed" });
|
|
443
443
|
this.advanceIdleIssue(issue, "delegated", {
|
|
444
444
|
pendingRunType: "implementation",
|
|
445
445
|
clearFailureProvenance: true,
|
|
@@ -481,7 +481,7 @@ export class IdleIssueReconciler {
|
|
|
481
481
|
}
|
|
482
482
|
const downstreamOwned = issue.factoryState === "awaiting_queue" || issue.prReviewState === "approved" || pr.reviewDecision === "APPROVED";
|
|
483
483
|
const mergeConflictDetected = pr.mergeable === "CONFLICTING" || pr.mergeStateStatus === "DIRTY";
|
|
484
|
-
const refreshedIssue = this.db.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
484
|
+
const refreshedIssue = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
485
485
|
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
486
486
|
prNumber: refreshedIssue.prNumber,
|
|
487
487
|
prState: refreshedIssue.prState,
|
|
@@ -549,7 +549,7 @@ export class IdleIssueReconciler {
|
|
|
549
549
|
return;
|
|
550
550
|
}
|
|
551
551
|
if (isReviewDecisionApproved(pr.reviewDecision)) {
|
|
552
|
-
this.db.upsertIssue({
|
|
552
|
+
this.db.issues.upsertIssue({
|
|
553
553
|
projectId: issue.projectId,
|
|
554
554
|
linearIssueId: issue.linearIssueId,
|
|
555
555
|
prReviewState: "approved",
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { ACTIVE_RUN_STATES } from "./factory-state.js";
|
|
2
|
+
import { buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
3
|
+
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
4
|
+
function isRequestedChangesRunType(runType) {
|
|
5
|
+
return runType === "review_fix" || runType === "branch_upkeep";
|
|
6
|
+
}
|
|
7
|
+
function resolveRetryRunType(runType, context) {
|
|
8
|
+
if (runType === "branch_upkeep") {
|
|
9
|
+
return "branch_upkeep";
|
|
10
|
+
}
|
|
11
|
+
return context?.reviewFixMode === "branch_upkeep" || context?.branchUpkeepRequired === true
|
|
12
|
+
? "branch_upkeep"
|
|
13
|
+
: "review_fix";
|
|
14
|
+
}
|
|
15
|
+
function resolvePostRunState(issue) {
|
|
16
|
+
if (ACTIVE_RUN_STATES.has(issue.factoryState) && issue.prNumber) {
|
|
17
|
+
if (issue.prState === "merged")
|
|
18
|
+
return "done";
|
|
19
|
+
if (issue.prReviewState === "approved")
|
|
20
|
+
return "awaiting_queue";
|
|
21
|
+
return "pr_open";
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
export function resolveRecoverablePostRunState(issue) {
|
|
26
|
+
if (!issue.prNumber) {
|
|
27
|
+
return resolvePostRunState(issue);
|
|
28
|
+
}
|
|
29
|
+
if (issue.prState === "merged")
|
|
30
|
+
return "done";
|
|
31
|
+
if (issue.prState === "open") {
|
|
32
|
+
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
33
|
+
prNumber: issue.prNumber,
|
|
34
|
+
prState: issue.prState,
|
|
35
|
+
prReviewState: issue.prReviewState,
|
|
36
|
+
prCheckStatus: issue.prCheckStatus,
|
|
37
|
+
latestFailureSource: issue.lastGitHubFailureSource,
|
|
38
|
+
});
|
|
39
|
+
if (reactiveIntent)
|
|
40
|
+
return reactiveIntent.compatibilityFactoryState;
|
|
41
|
+
if (issue.prReviewState === "approved")
|
|
42
|
+
return "awaiting_queue";
|
|
43
|
+
return "pr_open";
|
|
44
|
+
}
|
|
45
|
+
return resolvePostRunState(issue);
|
|
46
|
+
}
|
|
47
|
+
export class InterruptedRunRecovery {
|
|
48
|
+
db;
|
|
49
|
+
logger;
|
|
50
|
+
linearSync;
|
|
51
|
+
withHeldLease;
|
|
52
|
+
releaseLease;
|
|
53
|
+
failRunAndClear;
|
|
54
|
+
restoreIdleWorktree;
|
|
55
|
+
completionPolicy;
|
|
56
|
+
enqueueIssue;
|
|
57
|
+
feed;
|
|
58
|
+
constructor(db, logger, linearSync, withHeldLease, releaseLease, failRunAndClear, restoreIdleWorktree, completionPolicy, enqueueIssue, feed) {
|
|
59
|
+
this.db = db;
|
|
60
|
+
this.logger = logger;
|
|
61
|
+
this.linearSync = linearSync;
|
|
62
|
+
this.withHeldLease = withHeldLease;
|
|
63
|
+
this.releaseLease = releaseLease;
|
|
64
|
+
this.failRunAndClear = failRunAndClear;
|
|
65
|
+
this.restoreIdleWorktree = restoreIdleWorktree;
|
|
66
|
+
this.completionPolicy = completionPolicy;
|
|
67
|
+
this.enqueueIssue = enqueueIssue;
|
|
68
|
+
this.feed = feed;
|
|
69
|
+
}
|
|
70
|
+
async handle(run, issue) {
|
|
71
|
+
this.logger.warn({ issueKey: issue.issueKey, runType: run.runType, threadId: run.threadId }, "Run has interrupted turn - marking as failed");
|
|
72
|
+
const repairedCounters = this.withHeldLease(issue.projectId, issue.linearIssueId, (lease) => {
|
|
73
|
+
if (run.runType === "ci_repair" && issue.ciRepairAttempts > 0) {
|
|
74
|
+
this.db.issueSessions.upsertIssueWithLease(lease, {
|
|
75
|
+
projectId: issue.projectId,
|
|
76
|
+
linearIssueId: issue.linearIssueId,
|
|
77
|
+
ciRepairAttempts: issue.ciRepairAttempts - 1,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else if (run.runType === "queue_repair" && issue.queueRepairAttempts > 0) {
|
|
81
|
+
this.db.issueSessions.upsertIssueWithLease(lease, {
|
|
82
|
+
projectId: issue.projectId,
|
|
83
|
+
linearIssueId: issue.linearIssueId,
|
|
84
|
+
queueRepairAttempts: issue.queueRepairAttempts - 1,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (run.runType === "ci_repair" || run.runType === "queue_repair") {
|
|
88
|
+
this.db.issueSessions.upsertIssueWithLease(lease, {
|
|
89
|
+
projectId: issue.projectId,
|
|
90
|
+
linearIssueId: issue.linearIssueId,
|
|
91
|
+
lastAttemptedFailureHeadSha: null,
|
|
92
|
+
lastAttemptedFailureSignature: null,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
});
|
|
97
|
+
if (!repairedCounters) {
|
|
98
|
+
this.logger.warn({ runId: run.id, issueId: run.linearIssueId }, "Skipping interrupted-run recovery after losing issue-session lease");
|
|
99
|
+
this.releaseLease(run.projectId, run.linearIssueId);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (isRequestedChangesRunType(run.runType)) {
|
|
103
|
+
await this.handleInterruptedRequestedChangesRun(run, issue);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const recoveredState = resolveRecoverablePostRunState(this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? issue);
|
|
107
|
+
this.failRunAndClear(run, "Codex turn was interrupted", recoveredState);
|
|
108
|
+
await this.restoreIdleWorktree(issue);
|
|
109
|
+
const failedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
110
|
+
if (recoveredState) {
|
|
111
|
+
this.feed?.publish({
|
|
112
|
+
level: "info",
|
|
113
|
+
kind: "stage",
|
|
114
|
+
issueKey: issue.issueKey,
|
|
115
|
+
projectId: run.projectId,
|
|
116
|
+
stage: recoveredState,
|
|
117
|
+
status: "reconciled",
|
|
118
|
+
summary: `Interrupted ${run.runType} recovered -> ${recoveredState}`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
void this.linearSync.emitActivity(failedIssue, buildRunFailureActivity(run.runType, "The Codex turn was interrupted."));
|
|
123
|
+
}
|
|
124
|
+
void this.linearSync.syncSession(failedIssue, { activeRunType: run.runType });
|
|
125
|
+
this.releaseLease(run.projectId, run.linearIssueId);
|
|
126
|
+
}
|
|
127
|
+
async handleInterruptedRequestedChangesRun(run, issue) {
|
|
128
|
+
const freshIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
129
|
+
const refreshedIssue = await this.completionPolicy.refreshIssueAfterReactivePublish(run, freshIssue);
|
|
130
|
+
const retryContext = await this.completionPolicy.resolveRequestedChangesWakeContext(refreshedIssue, run.runType, run.runType === "branch_upkeep"
|
|
131
|
+
? {
|
|
132
|
+
branchUpkeepRequired: true,
|
|
133
|
+
reviewFixMode: "branch_upkeep",
|
|
134
|
+
wakeReason: "branch_upkeep",
|
|
135
|
+
}
|
|
136
|
+
: undefined);
|
|
137
|
+
const retryRunType = resolveRetryRunType(run.runType, retryContext);
|
|
138
|
+
const recoveredState = resolveRecoverablePostRunState(refreshedIssue) ?? "failed";
|
|
139
|
+
const interruptedMessage = "Requested-changes run was interrupted before PatchRelay could verify that a new PR head was published";
|
|
140
|
+
this.failRunAndClear(run, interruptedMessage, recoveredState);
|
|
141
|
+
await this.restoreIdleWorktree(issue);
|
|
142
|
+
const recoveredIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? refreshedIssue;
|
|
143
|
+
if (recoveredState === "changes_requested") {
|
|
144
|
+
this.db.issues.upsertIssue({
|
|
145
|
+
projectId: run.projectId,
|
|
146
|
+
linearIssueId: run.linearIssueId,
|
|
147
|
+
pendingRunType: retryRunType,
|
|
148
|
+
pendingRunContextJson: retryContext ? JSON.stringify(retryContext) : null,
|
|
149
|
+
});
|
|
150
|
+
this.feed?.publish({
|
|
151
|
+
level: "warn",
|
|
152
|
+
kind: "workflow",
|
|
153
|
+
issueKey: issue.issueKey,
|
|
154
|
+
projectId: run.projectId,
|
|
155
|
+
stage: run.runType,
|
|
156
|
+
status: "retry_queued",
|
|
157
|
+
summary: "Requested-changes run was interrupted; PatchRelay will retry from fresh GitHub truth",
|
|
158
|
+
});
|
|
159
|
+
this.enqueueIssue(run.projectId, run.linearIssueId);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
this.feed?.publish({
|
|
163
|
+
level: "error",
|
|
164
|
+
kind: "workflow",
|
|
165
|
+
issueKey: issue.issueKey,
|
|
166
|
+
projectId: run.projectId,
|
|
167
|
+
stage: run.runType,
|
|
168
|
+
status: "escalated",
|
|
169
|
+
summary: interruptedMessage,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
void this.linearSync.emitActivity(recoveredIssue, buildRunFailureActivity(run.runType, interruptedMessage));
|
|
173
|
+
void this.linearSync.syncSession(recoveredIssue, { activeRunType: run.runType });
|
|
174
|
+
this.releaseLease(run.projectId, run.linearIssueId);
|
|
175
|
+
}
|
|
176
|
+
}
|