patchrelay 0.36.9 → 0.36.10
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/data.js +3 -1
- package/dist/cli/formatters/text.js +3 -1
- package/dist/github-webhook-handler.js +17 -1
- package/dist/idle-reconciliation.js +11 -0
- package/dist/run-orchestrator.js +14 -0
- package/dist/run-recovery-service.js +7 -5
- package/dist/zombie-recovery.js +13 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli/data.js
CHANGED
|
@@ -221,6 +221,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
221
221
|
.reverse()
|
|
222
222
|
.map((run) => {
|
|
223
223
|
const summary = summarizeRun(run);
|
|
224
|
+
const eventCount = this.db.runs.listThreadEvents(run.id).length;
|
|
224
225
|
return {
|
|
225
226
|
runId: run.id,
|
|
226
227
|
runType: run.runType,
|
|
@@ -230,7 +231,8 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
230
231
|
...(run.parentThreadId ? { parentThreadId: run.parentThreadId } : {}),
|
|
231
232
|
...(summary ? { summary } : {}),
|
|
232
233
|
...(run.failureReason ? { failureReason: run.failureReason } : {}),
|
|
233
|
-
eventCount
|
|
234
|
+
eventCount,
|
|
235
|
+
eventCountAvailable: this.config.runner.codex.persistExtendedHistory || eventCount > 0,
|
|
234
236
|
startedAt: run.startedAt,
|
|
235
237
|
...(run.endedAt ? { endedAt: run.endedAt } : {}),
|
|
236
238
|
isCurrentThread: run.threadId !== undefined && run.threadId === dbIssue.threadId,
|
|
@@ -106,7 +106,9 @@ export function formatSessionHistory(result, buildOpenForThread) {
|
|
|
106
106
|
if (session.turnId) {
|
|
107
107
|
lines.push(value("Turn", session.turnId));
|
|
108
108
|
}
|
|
109
|
-
lines.push(value("Events", session.
|
|
109
|
+
lines.push(value("Events", session.eventCountAvailable
|
|
110
|
+
? session.eventCount
|
|
111
|
+
: "not persisted (persistExtendedHistory=false)"));
|
|
110
112
|
if (session.summary) {
|
|
111
113
|
lines.push(value("Summary", truncateLine(session.summary)));
|
|
112
114
|
}
|
|
@@ -136,6 +136,7 @@ export class GitHubWebhookHandler {
|
|
|
136
136
|
return;
|
|
137
137
|
}
|
|
138
138
|
const project = this.config.projects.find((p) => p.id === issue.projectId);
|
|
139
|
+
const immediateCheckStatus = this.deriveImmediatePrCheckStatus(issue, event, project);
|
|
139
140
|
// Update PR state on the issue
|
|
140
141
|
this.db.issues.upsertIssue({
|
|
141
142
|
projectId: issue.projectId,
|
|
@@ -146,7 +147,7 @@ export class GitHubWebhookHandler {
|
|
|
146
147
|
...(event.headSha !== undefined ? { prHeadSha: event.headSha } : {}),
|
|
147
148
|
...(event.prAuthorLogin !== undefined ? { prAuthorLogin: event.prAuthorLogin } : {}),
|
|
148
149
|
...(event.reviewState !== undefined ? { prReviewState: event.reviewState } : {}),
|
|
149
|
-
...(
|
|
150
|
+
...(immediateCheckStatus !== undefined ? { prCheckStatus: immediateCheckStatus } : {}),
|
|
150
151
|
...(event.reviewState === "changes_requested"
|
|
151
152
|
? { lastBlockingReviewHeadSha: event.reviewCommitId ?? event.headSha ?? null }
|
|
152
153
|
: event.reviewState === "approved"
|
|
@@ -730,6 +731,21 @@ export class GitHubWebhookHandler {
|
|
|
730
731
|
const normalized = event.checkName.trim().toLowerCase();
|
|
731
732
|
return this.getGateCheckNames(project).some((entry) => entry.trim().toLowerCase() === normalized);
|
|
732
733
|
}
|
|
734
|
+
deriveImmediatePrCheckStatus(issue, event, project) {
|
|
735
|
+
if (event.triggerEvent === "pr_synchronize") {
|
|
736
|
+
return "pending";
|
|
737
|
+
}
|
|
738
|
+
if (event.eventSource !== "check_run") {
|
|
739
|
+
return undefined;
|
|
740
|
+
}
|
|
741
|
+
if (!this.isGateCheckEvent(event, project)) {
|
|
742
|
+
return undefined;
|
|
743
|
+
}
|
|
744
|
+
if (this.isStaleGateEvent(issue, event)) {
|
|
745
|
+
return undefined;
|
|
746
|
+
}
|
|
747
|
+
return event.checkStatus;
|
|
748
|
+
}
|
|
733
749
|
isStaleGateEvent(issue, event) {
|
|
734
750
|
return Boolean(issue.lastGitHubCiSnapshotHeadSha
|
|
735
751
|
&& event.headSha
|
|
@@ -4,6 +4,7 @@ import { deriveGateCheckStatusFromRollup } from "./github-rollup.js";
|
|
|
4
4
|
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
5
5
|
import { parseStoredQueueRepairContext } from "./merge-queue-incident.js";
|
|
6
6
|
import { execCommand } from "./utils.js";
|
|
7
|
+
const DEFAULT_REVIEW_FIX_BUDGET = 12;
|
|
7
8
|
function isFailingCheckStatus(status) {
|
|
8
9
|
return status === "failed" || status === "failure";
|
|
9
10
|
}
|
|
@@ -493,6 +494,16 @@ export class IdleIssueReconciler {
|
|
|
493
494
|
});
|
|
494
495
|
if ((issue.factoryState === "escalated" || issue.factoryState === "failed")
|
|
495
496
|
&& (reactiveIntent?.runType === "review_fix" || reactiveIntent?.runType === "branch_upkeep")) {
|
|
497
|
+
if (issue.reviewFixAttempts >= DEFAULT_REVIEW_FIX_BUDGET) {
|
|
498
|
+
this.logger.debug({
|
|
499
|
+
issueKey: issue.issueKey,
|
|
500
|
+
prNumber: issue.prNumber,
|
|
501
|
+
from: issue.factoryState,
|
|
502
|
+
runType: reactiveIntent.runType,
|
|
503
|
+
reviewFixAttempts: issue.reviewFixAttempts,
|
|
504
|
+
}, "Reconciliation: leaving terminal requested-changes issue escalated because the repair budget is exhausted");
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
496
507
|
const pendingRunContext = reactiveIntent.runType === "branch_upkeep"
|
|
497
508
|
? buildBranchUpkeepContext(issue.prNumber, project.github?.baseBranch ?? "main", pr.mergeStateStatus, pr.headRefOid)
|
|
498
509
|
: undefined;
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -14,12 +14,20 @@ import { RunFinalizer } from "./run-finalizer.js";
|
|
|
14
14
|
import { RunLauncher } from "./run-launcher.js";
|
|
15
15
|
import { RunRecoveryService } from "./run-recovery-service.js";
|
|
16
16
|
import { RunWakePlanner } from "./run-wake-planner.js";
|
|
17
|
+
import { getRemainingZombieRecoveryDelayMs } from "./zombie-recovery.js";
|
|
17
18
|
function lowerCaseFirst(value) {
|
|
18
19
|
return value ? `${value.slice(0, 1).toLowerCase()}${value.slice(1)}` : value;
|
|
19
20
|
}
|
|
20
21
|
function isRequestedChangesRunType(runType) {
|
|
21
22
|
return runType === "review_fix" || runType === "branch_upkeep";
|
|
22
23
|
}
|
|
24
|
+
function shouldDelayZombieRecoveryLaunch(issue, issueSession, runType) {
|
|
25
|
+
if (issue.zombieRecoveryAttempts <= 0)
|
|
26
|
+
return 0;
|
|
27
|
+
if (issueSession?.lastRunType !== runType)
|
|
28
|
+
return 0;
|
|
29
|
+
return getRemainingZombieRecoveryDelayMs(issue.lastZombieRecoveryAt, issue.zombieRecoveryAttempts);
|
|
30
|
+
}
|
|
23
31
|
export class RunOrchestrator {
|
|
24
32
|
config;
|
|
25
33
|
db;
|
|
@@ -108,6 +116,12 @@ export class RunOrchestrator {
|
|
|
108
116
|
return;
|
|
109
117
|
}
|
|
110
118
|
const { runType, context, resumeThread } = wake;
|
|
119
|
+
const remainingZombieDelayMs = shouldDelayZombieRecoveryLaunch(issue, issueSession, runType);
|
|
120
|
+
if (remainingZombieDelayMs > 0) {
|
|
121
|
+
this.logger.debug({ issueKey: issue.issueKey, runType, remainingZombieDelayMs }, "Deferring recovered run launch until zombie backoff elapses");
|
|
122
|
+
this.releaseIssueSessionLease(item.projectId, item.issueId);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
111
125
|
const effectiveContext = isRequestedChangesRunType(runType)
|
|
112
126
|
? await this.runCompletionPolicy.resolveRequestedChangesWakeContext(issue, runType, context)
|
|
113
127
|
: context;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
2
|
+
import { getRemainingZombieRecoveryDelayMs } from "./zombie-recovery.js";
|
|
2
3
|
const DEFAULT_ZOMBIE_RECOVERY_BUDGET = 5;
|
|
3
|
-
const ZOMBIE_RECOVERY_BASE_DELAY_MS = 15_000;
|
|
4
4
|
export class RunRecoveryService {
|
|
5
5
|
db;
|
|
6
6
|
logger;
|
|
@@ -108,10 +108,12 @@ export class RunRecoveryService {
|
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
if (fresh.lastZombieRecoveryAt) {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
const remainingDelayMs = getRemainingZombieRecoveryDelayMs(fresh.lastZombieRecoveryAt, fresh.zombieRecoveryAttempts);
|
|
112
|
+
if (remainingDelayMs > 0) {
|
|
113
|
+
this.withHeldLease(fresh.projectId, fresh.linearIssueId, (lease) => {
|
|
114
|
+
this.appendWakeEventWithLease(lease, fresh, runType, undefined, `recovery:${attempts}`);
|
|
115
|
+
});
|
|
116
|
+
this.logger.debug({ issueKey: fresh.issueKey, attempts: fresh.zombieRecoveryAttempts, remainingDelayMs }, "Recovery: backoff not elapsed, deferring retry");
|
|
115
117
|
return;
|
|
116
118
|
}
|
|
117
119
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const ZOMBIE_RECOVERY_BASE_DELAY_MS = 15_000;
|
|
2
|
+
export function getZombieRecoveryDelayMs(recoveryAttempts) {
|
|
3
|
+
return ZOMBIE_RECOVERY_BASE_DELAY_MS * Math.pow(2, recoveryAttempts);
|
|
4
|
+
}
|
|
5
|
+
export function getRemainingZombieRecoveryDelayMs(lastRecoveryAt, recoveryAttempts, now = Date.now()) {
|
|
6
|
+
if (!lastRecoveryAt)
|
|
7
|
+
return 0;
|
|
8
|
+
const recoveredAtMs = Date.parse(lastRecoveryAt);
|
|
9
|
+
if (!Number.isFinite(recoveredAtMs))
|
|
10
|
+
return 0;
|
|
11
|
+
const delay = getZombieRecoveryDelayMs(recoveryAttempts);
|
|
12
|
+
return Math.max(0, recoveredAtMs + delay - now);
|
|
13
|
+
}
|