patchrelay 0.50.3 → 0.50.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.
package/dist/build-info.json
CHANGED
|
@@ -16,53 +16,74 @@ export class ImplementationOutcomePolicy {
|
|
|
16
16
|
}
|
|
17
17
|
const project = this.config.projects.find((entry) => entry.id === run.projectId);
|
|
18
18
|
const baseBranch = project?.github?.baseBranch ?? "main";
|
|
19
|
-
|
|
19
|
+
const publishedPrState = await this.detectPublishedPrState(issue, project?.github?.repoFullName);
|
|
20
|
+
if (publishedPrState === "open") {
|
|
20
21
|
return undefined;
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
23
|
+
const details = await this.describeLocalImplementationOutcome(issue, baseBranch);
|
|
24
|
+
return details ?? `Implementation completed without opening a PR for branch ${issue.branchName ?? issue.linearIssueId}`;
|
|
25
|
+
}
|
|
26
|
+
async detectRecoverableFailedImplementationOutcome(run, issue) {
|
|
27
|
+
if (run.runType !== "implementation") {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const project = this.config.projects.find((entry) => entry.id === run.projectId);
|
|
31
|
+
const publishedPrState = await this.detectPublishedPrState(issue, project?.github?.repoFullName);
|
|
32
|
+
if (publishedPrState === "open" || publishedPrState === "unknown") {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const baseBranch = project?.github?.baseBranch ?? "main";
|
|
36
|
+
return await this.describeLocalImplementationOutcome(issue, baseBranch);
|
|
37
|
+
}
|
|
38
|
+
async detectPublishedPrState(issue, repoFullName) {
|
|
39
|
+
if (issue.prNumber && issue.prState && issue.prState !== "closed") {
|
|
40
|
+
return "open";
|
|
41
|
+
}
|
|
42
|
+
if (!repoFullName || !issue.branchName) {
|
|
43
|
+
return "unknown";
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const { stdout, exitCode } = await execCommand("gh", [
|
|
47
|
+
"pr",
|
|
48
|
+
"list",
|
|
49
|
+
"--repo",
|
|
50
|
+
repoFullName,
|
|
51
|
+
"--head",
|
|
52
|
+
issue.branchName,
|
|
53
|
+
"--state",
|
|
54
|
+
"all",
|
|
55
|
+
"--json",
|
|
56
|
+
"number,url,state,author,headRefOid",
|
|
57
|
+
], { timeoutMs: 10_000 });
|
|
58
|
+
if (exitCode !== 0) {
|
|
59
|
+
return "unknown";
|
|
54
60
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
repoFullName: project.github.repoFullName,
|
|
60
|
-
error: error instanceof Error ? error.message : String(error),
|
|
61
|
-
}, "Failed to verify published PR state after implementation");
|
|
61
|
+
const matches = JSON.parse(stdout);
|
|
62
|
+
const pr = matches[0];
|
|
63
|
+
if (!pr?.number) {
|
|
64
|
+
return "none";
|
|
62
65
|
}
|
|
66
|
+
const state = pr.state?.toLowerCase();
|
|
67
|
+
this.upsertIssueIfLeaseHeld(issue.projectId, issue.linearIssueId, {
|
|
68
|
+
projectId: issue.projectId,
|
|
69
|
+
linearIssueId: issue.linearIssueId,
|
|
70
|
+
prNumber: pr.number,
|
|
71
|
+
...(pr.url ? { prUrl: pr.url } : {}),
|
|
72
|
+
...(state ? { prState: state } : {}),
|
|
73
|
+
...(pr.headRefOid ? { prHeadSha: pr.headRefOid } : {}),
|
|
74
|
+
...(pr.author?.login ? { prAuthorLogin: pr.author.login } : {}),
|
|
75
|
+
}, "published PR verification refresh");
|
|
76
|
+
return state === "closed" ? "closed" : "open";
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
this.logger.debug({
|
|
80
|
+
issueKey: issue.issueKey,
|
|
81
|
+
branchName: issue.branchName,
|
|
82
|
+
repoFullName,
|
|
83
|
+
error: error instanceof Error ? error.message : String(error),
|
|
84
|
+
}, "Failed to verify published PR state after implementation");
|
|
85
|
+
return "unknown";
|
|
63
86
|
}
|
|
64
|
-
const details = await this.describeLocalImplementationOutcome(issue, baseBranch);
|
|
65
|
-
return details ?? `Implementation completed without opening a PR for branch ${issue.branchName ?? issue.linearIssueId}`;
|
|
66
87
|
}
|
|
67
88
|
upsertIssueIfLeaseHeld(projectId, linearIssueId, params, context) {
|
|
68
89
|
const updated = this.withHeldLease(projectId, linearIssueId, (lease) => this.db.issueSessions.upsertIssueWithLease(lease, params));
|
|
@@ -8,10 +8,12 @@ function shouldContinueForUnpublishedLocalChanges(message) {
|
|
|
8
8
|
|| (normalized.includes("local commit") && normalized.includes("ahead of origin/"));
|
|
9
9
|
}
|
|
10
10
|
export async function handleNoPrCompletionCheck(params) {
|
|
11
|
-
const
|
|
11
|
+
const runUpdate = buildRunUpdate({
|
|
12
|
+
status: params.runStatus,
|
|
12
13
|
threadId: params.threadId,
|
|
13
14
|
...(params.completedTurnId ? { completedTurnId: params.completedTurnId } : {}),
|
|
14
15
|
report: params.report,
|
|
16
|
+
...(params.failureReason ? { failureReason: params.failureReason } : {}),
|
|
15
17
|
});
|
|
16
18
|
params.publishTurnEvent({
|
|
17
19
|
level: "info",
|
|
@@ -53,7 +55,7 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
53
55
|
}
|
|
54
56
|
if (completionCheck.outcome === "continue") {
|
|
55
57
|
const continued = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
56
|
-
params.db.runs.finishRun(params.run.id,
|
|
58
|
+
params.db.runs.finishRun(params.run.id, runUpdate);
|
|
57
59
|
params.db.runs.saveCompletionCheck(params.run.id, completionCheck);
|
|
58
60
|
params.db.issues.upsertIssue({
|
|
59
61
|
projectId: params.run.projectId,
|
|
@@ -93,7 +95,7 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
93
95
|
}
|
|
94
96
|
if (completionCheck.outcome === "needs_input") {
|
|
95
97
|
const completed = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
96
|
-
params.db.runs.finishRun(params.run.id,
|
|
98
|
+
params.db.runs.finishRun(params.run.id, runUpdate);
|
|
97
99
|
params.db.runs.saveCompletionCheck(params.run.id, completionCheck);
|
|
98
100
|
params.db.issueSessions.clearPendingIssueSessionEventsWithLease(lease);
|
|
99
101
|
params.db.issues.upsertIssue({
|
|
@@ -125,7 +127,7 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
125
127
|
if (completionCheck.outcome === "done") {
|
|
126
128
|
if (shouldContinueForUnpublishedLocalChanges(params.publishedOutcomeError)) {
|
|
127
129
|
const continued = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
128
|
-
params.db.runs.finishRun(params.run.id,
|
|
130
|
+
params.db.runs.finishRun(params.run.id, runUpdate);
|
|
129
131
|
params.db.runs.saveCompletionCheck(params.run.id, {
|
|
130
132
|
...completionCheck,
|
|
131
133
|
outcome: "continue",
|
|
@@ -172,7 +174,7 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
172
174
|
? params.db.issues.countOpenChildIssues(params.run.projectId, params.run.linearIssueId)
|
|
173
175
|
: 0;
|
|
174
176
|
const completed = params.withHeldLease(params.run.projectId, params.run.linearIssueId, (lease) => {
|
|
175
|
-
params.db.runs.finishRun(params.run.id,
|
|
177
|
+
params.db.runs.finishRun(params.run.id, runUpdate);
|
|
176
178
|
params.db.runs.saveCompletionCheck(params.run.id, completionCheck);
|
|
177
179
|
params.db.issueSessions.clearPendingIssueSessionEventsWithLease(lease);
|
|
178
180
|
params.db.issues.upsertIssue({
|
|
@@ -226,7 +228,7 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
226
228
|
const failureReason = `No PR observed and the completion check failed this run: ${completionCheck.summary}`;
|
|
227
229
|
const failed = params.withHeldLease(params.run.projectId, params.run.linearIssueId, () => {
|
|
228
230
|
params.db.runs.finishRun(params.run.id, {
|
|
229
|
-
...
|
|
231
|
+
...runUpdate,
|
|
230
232
|
status: "failed",
|
|
231
233
|
failureReason,
|
|
232
234
|
});
|
|
@@ -256,12 +258,13 @@ export async function handleNoPrCompletionCheck(params) {
|
|
|
256
258
|
detail: completionCheck.summary,
|
|
257
259
|
});
|
|
258
260
|
}
|
|
259
|
-
function
|
|
261
|
+
function buildRunUpdate(params) {
|
|
260
262
|
return {
|
|
261
|
-
status:
|
|
263
|
+
status: params.status,
|
|
262
264
|
threadId: params.threadId,
|
|
263
265
|
...(params.completedTurnId ? { turnId: params.completedTurnId } : {}),
|
|
264
266
|
summaryJson: JSON.stringify({ latestAssistantMessage: params.report.assistantMessages.at(-1) ?? null }),
|
|
265
267
|
reportJson: JSON.stringify(params.report),
|
|
268
|
+
...(params.failureReason ? { failureReason: params.failureReason } : {}),
|
|
266
269
|
};
|
|
267
270
|
}
|
|
@@ -39,4 +39,7 @@ export class RunCompletionPolicy {
|
|
|
39
39
|
async verifyPublishedRunOutcome(run, issue) {
|
|
40
40
|
return await this.implementationOutcomes.verifyPublishedRunOutcome(run, issue);
|
|
41
41
|
}
|
|
42
|
+
async detectRecoverableFailedImplementationOutcome(run, issue) {
|
|
43
|
+
return await this.implementationOutcomes.detectRecoverableFailedImplementationOutcome(run, issue);
|
|
44
|
+
}
|
|
42
45
|
}
|
package/dist/run-finalizer.js
CHANGED
|
@@ -126,6 +126,7 @@ export class RunFinalizer {
|
|
|
126
126
|
run,
|
|
127
127
|
issue: freshIssue,
|
|
128
128
|
report,
|
|
129
|
+
runStatus: "completed",
|
|
129
130
|
threadId,
|
|
130
131
|
...(params.completedTurnId ? { completedTurnId: params.completedTurnId } : {}),
|
|
131
132
|
publishedOutcomeError,
|
|
@@ -220,4 +221,34 @@ export class RunFinalizer {
|
|
|
220
221
|
this.linearSync.clearProgress(run.id);
|
|
221
222
|
this.releaseLease(run.projectId, run.linearIssueId);
|
|
222
223
|
}
|
|
224
|
+
async recoverFailedImplementationRun(params) {
|
|
225
|
+
const freshIssue = this.db.issues.getIssue(params.run.projectId, params.run.linearIssueId) ?? params.issue;
|
|
226
|
+
const publishedOutcomeError = await this.completionPolicy.detectRecoverableFailedImplementationOutcome(params.run, freshIssue);
|
|
227
|
+
if (!publishedOutcomeError) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
const trackedIssue = this.db.issueToTrackedIssue(freshIssue);
|
|
231
|
+
const report = buildStageReport({ ...params.run, status: "failed" }, trackedIssue, params.thread, countEventMethods(this.db.runs.listThreadEvents(params.run.id)));
|
|
232
|
+
await handleNoPrCompletionCheck({
|
|
233
|
+
db: this.db,
|
|
234
|
+
logger: this.logger,
|
|
235
|
+
withHeldLease: this.withHeldLease,
|
|
236
|
+
completionCheck: this.completionCheck,
|
|
237
|
+
run: params.run,
|
|
238
|
+
issue: freshIssue,
|
|
239
|
+
report,
|
|
240
|
+
runStatus: "failed",
|
|
241
|
+
threadId: params.threadId,
|
|
242
|
+
...(params.completedTurnId ? { completedTurnId: params.completedTurnId } : {}),
|
|
243
|
+
failureReason: params.failureReason,
|
|
244
|
+
publishedOutcomeError,
|
|
245
|
+
failRunAndClear: this.failRunAndClear,
|
|
246
|
+
emitActivity: (issueRecord, activity, options) => this.linearSync.emitActivity(issueRecord, activity, options),
|
|
247
|
+
publishTurnEvent: (event) => this.publishTurnEvent(event),
|
|
248
|
+
syncFailureOutcome: (event) => this.syncFailureOutcome(event),
|
|
249
|
+
syncCompletionCheckOutcome: (event) => this.syncCompletionCheckOutcome(event),
|
|
250
|
+
clearProgressAndRelease: (releaseRun) => this.clearProgressAndRelease(releaseRun),
|
|
251
|
+
});
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
223
254
|
}
|
|
@@ -75,13 +75,26 @@ export class RunNotificationHandler {
|
|
|
75
75
|
const completedTurnId = extractTurnId(notification.params);
|
|
76
76
|
const status = resolveRunCompletionStatus(notification.params);
|
|
77
77
|
if (status === "failed") {
|
|
78
|
+
const failureReason = "Codex reported the turn completed in a failed state";
|
|
79
|
+
const recovered = await this.runFinalizer.recoverFailedImplementationRun({
|
|
80
|
+
run,
|
|
81
|
+
issue,
|
|
82
|
+
thread,
|
|
83
|
+
threadId,
|
|
84
|
+
...(completedTurnId ? { completedTurnId } : {}),
|
|
85
|
+
failureReason,
|
|
86
|
+
});
|
|
87
|
+
if (recovered) {
|
|
88
|
+
this.activeThreadId = undefined;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
78
91
|
const nextState = isRequestedChangesRunType(run.runType) ? "escalated" : "failed";
|
|
79
92
|
const updated = this.withHeldIssueSessionLease(run.projectId, run.linearIssueId, (lease) => {
|
|
80
93
|
this.db.issueSessions.finishRunWithLease(lease, run.id, {
|
|
81
94
|
status: "failed",
|
|
82
95
|
threadId,
|
|
83
96
|
...(completedTurnId ? { turnId: completedTurnId } : {}),
|
|
84
|
-
failureReason
|
|
97
|
+
failureReason,
|
|
85
98
|
});
|
|
86
99
|
this.db.issueSessions.upsertIssueWithLease(lease, {
|
|
87
100
|
projectId: run.projectId,
|