patchrelay 0.51.4 → 0.52.0
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/github-webhook-reactive-run.js +14 -5
- package/dist/idle-reconciliation.js +24 -0
- package/dist/linear-progress-facts.js +18 -5
- package/dist/linear-progress-reporter.js +66 -9
- package/dist/run-finalizer.js +21 -1
- package/dist/service-startup-recovery.js +65 -11
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -5,8 +5,6 @@ import { buildGitHubQueueFailureContext, getRelevantGitHubCiSnapshot, resolveGit
|
|
|
5
5
|
import { isQueueEvictionFailure, isSettledBranchFailure } from "./github-webhook-policy.js";
|
|
6
6
|
export async function maybeEnqueueGitHubReactiveRun(params) {
|
|
7
7
|
const { issue, event, project, logger, feed, enqueueIssue, db, fetchImpl, failureContextResolver } = params;
|
|
8
|
-
if (issue.activeRunId !== undefined)
|
|
9
|
-
return;
|
|
10
8
|
if (isIssueTerminal(issue))
|
|
11
9
|
return;
|
|
12
10
|
if (!issue.delegatedToPatchRelay) {
|
|
@@ -22,6 +20,9 @@ export async function maybeEnqueueGitHubReactiveRun(params) {
|
|
|
22
20
|
return;
|
|
23
21
|
}
|
|
24
22
|
if (event.triggerEvent === "check_failed" && issue.prState === "open") {
|
|
23
|
+
if (issue.activeRunId !== undefined) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
25
26
|
await handleCheckFailedEvent({
|
|
26
27
|
db,
|
|
27
28
|
logger,
|
|
@@ -175,8 +176,14 @@ async function handleRequestedChangesEvent(params) {
|
|
|
175
176
|
});
|
|
176
177
|
const queuedRunType = hadPendingWake
|
|
177
178
|
? db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)?.runType
|
|
178
|
-
:
|
|
179
|
-
|
|
179
|
+
: issue.activeRunId === undefined
|
|
180
|
+
? enqueuePendingSessionWake(db, enqueueIssue, issue.projectId, issue.linearIssueId)
|
|
181
|
+
: db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)?.runType;
|
|
182
|
+
logger.info({
|
|
183
|
+
issueKey: issue.issueKey,
|
|
184
|
+
reviewerName: event.reviewerName,
|
|
185
|
+
deferredUntilRunRelease: issue.activeRunId !== undefined,
|
|
186
|
+
}, "Captured requested-changes follow-up");
|
|
180
187
|
feed?.publish({
|
|
181
188
|
level: "warn",
|
|
182
189
|
kind: "github",
|
|
@@ -184,7 +191,9 @@ async function handleRequestedChangesEvent(params) {
|
|
|
184
191
|
projectId: issue.projectId,
|
|
185
192
|
stage: "changes_requested",
|
|
186
193
|
status: "review_fix_queued",
|
|
187
|
-
summary:
|
|
194
|
+
summary: issue.activeRunId === undefined
|
|
195
|
+
? `${queuedRunType ?? "review_fix"} queued after requested changes`
|
|
196
|
+
: `${queuedRunType ?? "review_fix"} recorded and will resume after the active run finishes`,
|
|
188
197
|
detail: reviewComments && reviewComments.length > 0
|
|
189
198
|
? `${reviewComments.length} inline review comment${reviewComments.length === 1 ? "" : "s"} captured`
|
|
190
199
|
: event.reviewBody?.slice(0, 200) ?? event.reviewerName,
|
|
@@ -603,6 +603,30 @@ export class IdleIssueReconciler {
|
|
|
603
603
|
});
|
|
604
604
|
return;
|
|
605
605
|
}
|
|
606
|
+
if (issue.delegatedToPatchRelay
|
|
607
|
+
&& reactiveIntent?.runType === "review_fix"
|
|
608
|
+
&& this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) === undefined) {
|
|
609
|
+
this.logger.info({
|
|
610
|
+
issueKey: issue.issueKey,
|
|
611
|
+
prNumber: issue.prNumber,
|
|
612
|
+
from: issue.factoryState,
|
|
613
|
+
runType: reactiveIntent.runType,
|
|
614
|
+
}, "Reconciliation: re-queued requested-changes follow-up from GitHub truth");
|
|
615
|
+
this.advanceIdleIssue(issue, reactiveIntent.compatibilityFactoryState, {
|
|
616
|
+
pendingRunType: reactiveIntent.runType,
|
|
617
|
+
clearFailureProvenance: true,
|
|
618
|
+
});
|
|
619
|
+
this.feed?.publish({
|
|
620
|
+
level: "warn",
|
|
621
|
+
kind: "github",
|
|
622
|
+
issueKey: issue.issueKey,
|
|
623
|
+
projectId: issue.projectId,
|
|
624
|
+
stage: reactiveIntent.compatibilityFactoryState,
|
|
625
|
+
status: "review_fix_queued",
|
|
626
|
+
summary: `PR #${issue.prNumber} still has requested changes on the current head, dispatching review fix`,
|
|
627
|
+
});
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
606
630
|
if (issue.delegatedToPatchRelay && reactiveIntent?.runType === "branch_upkeep" && mergeConflictDetected) {
|
|
607
631
|
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, mergeable: pr.mergeable, mergeStateStatus: pr.mergeStateStatus }, "Reconciliation: PR still needs branch upkeep after requested changes");
|
|
608
632
|
this.advanceIdleIssue(issue, reactiveIntent.compatibilityFactoryState, {
|
|
@@ -26,21 +26,24 @@ function deriveProgressFactFromCompletedItem(rawItem, issue) {
|
|
|
26
26
|
return {
|
|
27
27
|
kind: "verification_started",
|
|
28
28
|
meaningKey: `verification:${normalizeMeaningKey(body)}`,
|
|
29
|
-
|
|
29
|
+
ephemeralContent: { type: "thought", body },
|
|
30
|
+
historyContent: { type: "thought", body },
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
if (looksLikePublishing(body)) {
|
|
33
34
|
return {
|
|
34
35
|
kind: "publishing_started",
|
|
35
36
|
meaningKey: `publishing:${normalizeMeaningKey(body)}`,
|
|
36
|
-
|
|
37
|
+
ephemeralContent: { type: "thought", body },
|
|
38
|
+
historyContent: { type: "thought", body },
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
if (looksLikeRootCause(body)) {
|
|
40
42
|
return {
|
|
41
43
|
kind: "root_cause_found",
|
|
42
44
|
meaningKey: `finding:${normalizeMeaningKey(body)}`,
|
|
43
|
-
|
|
45
|
+
ephemeralContent: { type: "thought", body },
|
|
46
|
+
historyContent: { type: "thought", body },
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
return undefined;
|
|
@@ -59,7 +62,12 @@ function deriveProgressFactFromPlan(rawPlan, issue) {
|
|
|
59
62
|
return {
|
|
60
63
|
kind: "verification_started",
|
|
61
64
|
meaningKey: `verification:${normalizeMeaningKey(activeStep.step)}`,
|
|
62
|
-
|
|
65
|
+
ephemeralContent: {
|
|
66
|
+
type: "action",
|
|
67
|
+
action: "Verifying",
|
|
68
|
+
parameter: summarizePlanStep(activeStep.step, "latest changes before publishing"),
|
|
69
|
+
},
|
|
70
|
+
historyContent: {
|
|
63
71
|
type: "action",
|
|
64
72
|
action: "Verifying",
|
|
65
73
|
parameter: summarizePlanStep(activeStep.step, "latest changes before publishing"),
|
|
@@ -71,7 +79,12 @@ function deriveProgressFactFromPlan(rawPlan, issue) {
|
|
|
71
79
|
return {
|
|
72
80
|
kind: "publishing_started",
|
|
73
81
|
meaningKey: `publishing:${normalizeMeaningKey(activeStep.step)}`,
|
|
74
|
-
|
|
82
|
+
ephemeralContent: {
|
|
83
|
+
type: "action",
|
|
84
|
+
action: "Publishing",
|
|
85
|
+
parameter,
|
|
86
|
+
},
|
|
87
|
+
historyContent: {
|
|
75
88
|
type: "action",
|
|
76
89
|
action: "Publishing",
|
|
77
90
|
parameter,
|
|
@@ -17,22 +17,79 @@ export class LinearProgressReporter {
|
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
const previous = this.publicationsByRun.get(run.id);
|
|
20
|
-
|
|
20
|
+
const shouldEmitEphemeral = previous?.ephemeralMeaningKey !== fact.meaningKey;
|
|
21
|
+
const shouldEmitHistory = previous?.historyMeaningKey !== fact.meaningKey;
|
|
22
|
+
if (!shouldEmitEphemeral && !shouldEmitHistory) {
|
|
21
23
|
return;
|
|
22
24
|
}
|
|
25
|
+
const now = Date.now();
|
|
23
26
|
const publication = {
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
...previous,
|
|
28
|
+
...(shouldEmitEphemeral
|
|
29
|
+
? {
|
|
30
|
+
ephemeralMeaningKey: fact.meaningKey,
|
|
31
|
+
ephemeralPublishedAtMs: now,
|
|
32
|
+
}
|
|
33
|
+
: {}),
|
|
34
|
+
...(shouldEmitHistory
|
|
35
|
+
? {
|
|
36
|
+
historyMeaningKey: fact.meaningKey,
|
|
37
|
+
historyPublishedAtMs: now,
|
|
38
|
+
}
|
|
39
|
+
: {}),
|
|
26
40
|
};
|
|
27
41
|
this.publicationsByRun.set(run.id, publication);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
if (shouldEmitEphemeral) {
|
|
43
|
+
void this.emitActivity(issue, fact.ephemeralContent, { ephemeral: true }).catch(() => {
|
|
44
|
+
this.clearFailedPublication(run.id, "ephemeral", fact.meaningKey, now);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (shouldEmitHistory) {
|
|
48
|
+
void this.emitActivity(issue, fact.historyContent).catch(() => {
|
|
49
|
+
this.clearFailedPublication(run.id, "history", fact.meaningKey, now);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
34
52
|
}
|
|
35
53
|
clearProgress(runId) {
|
|
36
54
|
this.publicationsByRun.delete(runId);
|
|
37
55
|
}
|
|
56
|
+
clearFailedPublication(runId, channel, meaningKey, publishedAtMs) {
|
|
57
|
+
const current = this.publicationsByRun.get(runId);
|
|
58
|
+
if (!current) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (channel === "ephemeral") {
|
|
62
|
+
if (current.ephemeralMeaningKey !== meaningKey || current.ephemeralPublishedAtMs !== publishedAtMs) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const next = {};
|
|
66
|
+
if (current.historyMeaningKey !== undefined) {
|
|
67
|
+
next.historyMeaningKey = current.historyMeaningKey;
|
|
68
|
+
}
|
|
69
|
+
if (current.historyPublishedAtMs !== undefined) {
|
|
70
|
+
next.historyPublishedAtMs = current.historyPublishedAtMs;
|
|
71
|
+
}
|
|
72
|
+
if (!next.historyMeaningKey) {
|
|
73
|
+
this.publicationsByRun.delete(runId);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.publicationsByRun.set(runId, next);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (current.historyMeaningKey !== meaningKey || current.historyPublishedAtMs !== publishedAtMs) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const next = {};
|
|
83
|
+
if (current.ephemeralMeaningKey !== undefined) {
|
|
84
|
+
next.ephemeralMeaningKey = current.ephemeralMeaningKey;
|
|
85
|
+
}
|
|
86
|
+
if (current.ephemeralPublishedAtMs !== undefined) {
|
|
87
|
+
next.ephemeralPublishedAtMs = current.ephemeralPublishedAtMs;
|
|
88
|
+
}
|
|
89
|
+
if (!next.ephemeralMeaningKey) {
|
|
90
|
+
this.publicationsByRun.delete(runId);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.publicationsByRun.set(runId, next);
|
|
94
|
+
}
|
|
38
95
|
}
|
package/dist/run-finalizer.js
CHANGED
|
@@ -135,6 +135,26 @@ export class RunFinalizer {
|
|
|
135
135
|
this.linearSync.clearProgress(run.id);
|
|
136
136
|
this.releaseLease(run.projectId, run.linearIssueId);
|
|
137
137
|
}
|
|
138
|
+
enqueuePendingWakeIfPresent(params) {
|
|
139
|
+
const wake = this.db.issueSessions.peekIssueSessionWake(params.run.projectId, params.run.linearIssueId);
|
|
140
|
+
if (!wake)
|
|
141
|
+
return undefined;
|
|
142
|
+
this.enqueueIssue(params.run.projectId, params.run.linearIssueId);
|
|
143
|
+
this.feed?.publish({
|
|
144
|
+
level: "info",
|
|
145
|
+
kind: "stage",
|
|
146
|
+
issueKey: params.issueKey,
|
|
147
|
+
projectId: params.run.projectId,
|
|
148
|
+
stage: wake.runType,
|
|
149
|
+
status: "deferred_follow_up_queued",
|
|
150
|
+
summary: `${wake.runType} queued after ${params.run.runType} released authority`,
|
|
151
|
+
...(wake.wakeReason ? { detail: `wake reason: ${wake.wakeReason}` } : {}),
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
runType: wake.runType,
|
|
155
|
+
...(wake.wakeReason ? { wakeReason: wake.wakeReason } : {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
138
158
|
publishTurnEvent(params) {
|
|
139
159
|
this.feed?.publish({
|
|
140
160
|
level: params.level,
|
|
@@ -294,7 +314,6 @@ export class RunFinalizer {
|
|
|
294
314
|
status: "follow_up_queued",
|
|
295
315
|
summary: postRunFollowUp.summary,
|
|
296
316
|
});
|
|
297
|
-
this.enqueueIssue(run.projectId, run.linearIssueId);
|
|
298
317
|
}
|
|
299
318
|
this.publishTurnEvent({
|
|
300
319
|
level: "info",
|
|
@@ -320,6 +339,7 @@ export class RunFinalizer {
|
|
|
320
339
|
void this.linearSync.emitActivity(updatedIssue, linearActivity);
|
|
321
340
|
}
|
|
322
341
|
void this.linearSync.syncSession(updatedIssue);
|
|
342
|
+
this.enqueuePendingWakeIfPresent({ run, issueKey: updatedIssue.issueKey });
|
|
323
343
|
this.linearSync.clearProgress(run.id);
|
|
324
344
|
this.releaseLease(run.projectId, run.linearIssueId);
|
|
325
345
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { appendDelegationObservedEvent } from "./delegation-audit.js";
|
|
2
|
+
import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
2
3
|
import { isResumablePausedLocalWork } from "./paused-issue-state.js";
|
|
3
4
|
export class ServiceStartupRecovery {
|
|
4
5
|
db;
|
|
@@ -89,6 +90,7 @@ export class ServiceStartupRecovery {
|
|
|
89
90
|
}
|
|
90
91
|
const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
91
92
|
const latestRun = this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
93
|
+
const hasPendingWake = this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined;
|
|
92
94
|
const shouldRecoverPausedLocalWork = delegated
|
|
93
95
|
&& isResumablePausedLocalWork({
|
|
94
96
|
issue: {
|
|
@@ -97,7 +99,21 @@ export class ServiceStartupRecovery {
|
|
|
97
99
|
},
|
|
98
100
|
latestRun,
|
|
99
101
|
})
|
|
100
|
-
&&
|
|
102
|
+
&& !hasPendingWake;
|
|
103
|
+
const reactiveIntent = delegated && !hasPendingWake
|
|
104
|
+
? deriveIssueSessionReactiveIntent({
|
|
105
|
+
delegatedToPatchRelay: delegated,
|
|
106
|
+
prNumber: issue.prNumber,
|
|
107
|
+
prState: issue.prState,
|
|
108
|
+
prIsDraft: issue.prIsDraft,
|
|
109
|
+
prReviewState: issue.prReviewState,
|
|
110
|
+
prCheckStatus: issue.prCheckStatus,
|
|
111
|
+
latestFailureSource: issue.lastGitHubFailureSource,
|
|
112
|
+
})
|
|
113
|
+
: undefined;
|
|
114
|
+
const shouldRecoverReactivePrWork = delegated
|
|
115
|
+
&& issue.prNumber !== undefined
|
|
116
|
+
&& reactiveIntent !== undefined;
|
|
101
117
|
const updated = this.db.issues.upsertIssue({
|
|
102
118
|
projectId: issue.projectId,
|
|
103
119
|
linearIssueId: issue.linearIssueId,
|
|
@@ -110,26 +126,64 @@ export class ServiceStartupRecovery {
|
|
|
110
126
|
...(liveIssue.estimate != null ? { estimate: liveIssue.estimate } : {}),
|
|
111
127
|
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
112
128
|
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
113
|
-
...(shouldRecoverPausedLocalWork
|
|
129
|
+
...(shouldRecoverPausedLocalWork
|
|
130
|
+
? { factoryState: "delegated" }
|
|
131
|
+
: shouldRecoverReactivePrWork
|
|
132
|
+
? { factoryState: reactiveIntent.compatibilityFactoryState }
|
|
133
|
+
: {}),
|
|
114
134
|
});
|
|
115
|
-
if (!shouldRecoverPausedLocalWork) {
|
|
135
|
+
if (!shouldRecoverPausedLocalWork && !shouldRecoverReactivePrWork) {
|
|
116
136
|
continue;
|
|
117
137
|
}
|
|
118
138
|
if (unresolvedBlockers === 0) {
|
|
119
|
-
|
|
120
|
-
projectId
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
if (shouldRecoverReactivePrWork && reactiveIntent) {
|
|
140
|
+
this.appendReactiveWakeEvent(issue.projectId, issue.linearIssueId, issue, reactiveIntent.runType);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
144
|
+
projectId: issue.projectId,
|
|
145
|
+
linearIssueId: issue.linearIssueId,
|
|
146
|
+
eventType: "delegated",
|
|
147
|
+
dedupeKey: `delegated:${issue.linearIssueId}`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
125
150
|
if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
126
151
|
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
127
152
|
}
|
|
128
|
-
this.logger.info({
|
|
153
|
+
this.logger.info({
|
|
154
|
+
issueKey: updated.issueKey,
|
|
155
|
+
...(shouldRecoverReactivePrWork && reactiveIntent ? { runType: reactiveIntent.runType } : {}),
|
|
156
|
+
}, shouldRecoverReactivePrWork
|
|
157
|
+
? "Recovered delegated PR issue from reactive paused state and re-queued follow-up work"
|
|
158
|
+
: "Recovered delegated issue from paused local-work state and re-queued implementation");
|
|
129
159
|
}
|
|
130
160
|
else {
|
|
131
|
-
this.logger.info({
|
|
161
|
+
this.logger.info({
|
|
162
|
+
issueKey: updated.issueKey,
|
|
163
|
+
unresolvedBlockers,
|
|
164
|
+
...(shouldRecoverReactivePrWork && reactiveIntent ? { runType: reactiveIntent.runType } : {}),
|
|
165
|
+
}, shouldRecoverReactivePrWork
|
|
166
|
+
? "Recovered delegated blocked PR issue from reactive paused state"
|
|
167
|
+
: "Recovered delegated blocked issue from paused local-work state");
|
|
132
168
|
}
|
|
133
169
|
}
|
|
134
170
|
}
|
|
171
|
+
appendReactiveWakeEvent(projectId, linearIssueId, issue, runType) {
|
|
172
|
+
const eventType = runType === "queue_repair"
|
|
173
|
+
? "merge_steward_incident"
|
|
174
|
+
: runType === "ci_repair"
|
|
175
|
+
? "settled_red_ci"
|
|
176
|
+
: "review_changes_requested";
|
|
177
|
+
const dedupeKey = runType === "queue_repair"
|
|
178
|
+
? `startup_recovery:queue_repair:${linearIssueId}:${issue.lastGitHubFailureSignature ?? issue.prHeadSha ?? issue.lastGitHubFailureHeadSha ?? "unknown"}`
|
|
179
|
+
: runType === "ci_repair"
|
|
180
|
+
? `startup_recovery:ci_repair:${linearIssueId}:${issue.lastGitHubFailureSignature ?? issue.prHeadSha ?? issue.lastGitHubFailureHeadSha ?? "unknown"}`
|
|
181
|
+
: `startup_recovery:${runType}:${linearIssueId}:${issue.prHeadSha ?? "unknown"}`;
|
|
182
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(projectId, linearIssueId, {
|
|
183
|
+
projectId,
|
|
184
|
+
linearIssueId,
|
|
185
|
+
eventType,
|
|
186
|
+
dedupeKey,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
135
189
|
}
|