patchrelay 0.36.17 → 0.36.19
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/issue-query-service.js +5 -44
- package/dist/linear-agent-session-client.js +109 -0
- package/dist/linear-progress-reporter.js +185 -0
- package/dist/linear-session-sync.js +23 -519
- package/dist/linear-status-comment-sync.js +152 -0
- package/dist/linear-workflow-state-sync.js +103 -0
- package/dist/merged-linear-completion-reconciler.js +48 -0
- package/dist/no-pr-completion-check.js +199 -0
- package/dist/operator-retry-event.js +58 -0
- package/dist/public-agent-session-status-query.js +52 -0
- package/dist/run-finalizer.js +72 -237
- package/dist/run-orchestrator.js +4 -39
- package/dist/service-issue-actions.js +164 -0
- package/dist/service-startup-recovery.js +104 -0
- package/dist/service.js +15 -556
- package/dist/tracked-issue-list-query.js +259 -0
- package/package.json +1 -1
package/dist/run-finalizer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildStageReport, countEventMethods } from "./run-reporting.js";
|
|
2
|
-
import {
|
|
2
|
+
import { buildRunCompletedActivity, buildRunFailureActivity } from "./linear-session-reporting.js";
|
|
3
|
+
import { handleNoPrCompletionCheck } from "./no-pr-completion-check.js";
|
|
3
4
|
import { resolveCompletedRunState } from "./run-completion-policy.js";
|
|
4
5
|
export class RunFinalizer {
|
|
5
6
|
db;
|
|
@@ -39,6 +40,50 @@ export class RunFinalizer {
|
|
|
39
40
|
this.linearSync.clearProgress(run.id);
|
|
40
41
|
this.releaseLease(run.projectId, run.linearIssueId);
|
|
41
42
|
}
|
|
43
|
+
publishTurnEvent(params) {
|
|
44
|
+
this.feed?.publish({
|
|
45
|
+
level: params.level,
|
|
46
|
+
kind: "turn",
|
|
47
|
+
issueKey: params.issueKey,
|
|
48
|
+
projectId: params.run.projectId,
|
|
49
|
+
stage: params.run.runType,
|
|
50
|
+
status: params.status,
|
|
51
|
+
summary: params.summary,
|
|
52
|
+
...(params.detail ? { detail: params.detail } : {}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
syncFailureOutcome(params) {
|
|
56
|
+
const issue = this.db.issues.getIssue(params.run.projectId, params.run.linearIssueId) ?? params.fallbackIssue;
|
|
57
|
+
this.publishTurnEvent({
|
|
58
|
+
level: params.level,
|
|
59
|
+
run: params.run,
|
|
60
|
+
issueKey: params.fallbackIssue.issueKey,
|
|
61
|
+
status: params.status,
|
|
62
|
+
summary: params.summary,
|
|
63
|
+
...(params.detail ? { detail: params.detail } : {}),
|
|
64
|
+
});
|
|
65
|
+
void this.linearSync.emitActivity(issue, buildRunFailureActivity(params.run.runType, params.message));
|
|
66
|
+
void this.linearSync.syncSession(issue, { activeRunType: params.run.runType });
|
|
67
|
+
this.clearProgressAndRelease(params.run);
|
|
68
|
+
}
|
|
69
|
+
syncCompletionCheckOutcome(params) {
|
|
70
|
+
const issue = this.db.issues.getIssue(params.run.projectId, params.run.linearIssueId) ?? params.fallbackIssue;
|
|
71
|
+
this.publishTurnEvent({
|
|
72
|
+
level: params.level,
|
|
73
|
+
run: params.run,
|
|
74
|
+
issueKey: params.fallbackIssue.issueKey,
|
|
75
|
+
status: params.status,
|
|
76
|
+
summary: params.summary,
|
|
77
|
+
...(params.detail ? { detail: params.detail } : {}),
|
|
78
|
+
});
|
|
79
|
+
void this.linearSync.emitActivity(issue, params.activity, { ephemeral: true });
|
|
80
|
+
void this.linearSync.syncSession(issue);
|
|
81
|
+
this.linearSync.clearProgress(params.run.id);
|
|
82
|
+
if (params.enqueue) {
|
|
83
|
+
this.enqueueIssue(params.run.projectId, params.run.linearIssueId);
|
|
84
|
+
}
|
|
85
|
+
this.releaseLease(params.run.projectId, params.run.linearIssueId);
|
|
86
|
+
}
|
|
42
87
|
async finalizeCompletedRun(params) {
|
|
43
88
|
const { run, issue, thread, threadId } = params;
|
|
44
89
|
const trackedIssue = this.db.issueToTrackedIssue(issue);
|
|
@@ -48,257 +93,49 @@ export class RunFinalizer {
|
|
|
48
93
|
if (verifiedRepairError) {
|
|
49
94
|
const holdState = params.resolveRecoverableRunState(freshIssue) ?? "failed";
|
|
50
95
|
this.failRunAndClear(run, verifiedRepairError, holdState);
|
|
51
|
-
|
|
52
|
-
|
|
96
|
+
this.syncFailureOutcome({
|
|
97
|
+
run,
|
|
98
|
+
fallbackIssue: freshIssue,
|
|
99
|
+
message: verifiedRepairError,
|
|
53
100
|
level: "warn",
|
|
54
|
-
kind: "turn",
|
|
55
|
-
issueKey: freshIssue.issueKey,
|
|
56
|
-
projectId: run.projectId,
|
|
57
|
-
stage: run.runType,
|
|
58
101
|
status: "branch_not_advanced",
|
|
59
102
|
summary: verifiedRepairError,
|
|
60
103
|
});
|
|
61
|
-
void this.linearSync.emitActivity(heldIssue, buildRunFailureActivity(run.runType, verifiedRepairError));
|
|
62
|
-
void this.linearSync.syncSession(heldIssue, { activeRunType: run.runType });
|
|
63
|
-
this.linearSync.clearProgress(run.id);
|
|
64
|
-
this.releaseLease(run.projectId, run.linearIssueId);
|
|
65
104
|
return;
|
|
66
105
|
}
|
|
67
106
|
const missingReviewFixHeadError = await this.completionPolicy.verifyReviewFixAdvancedHead(run, freshIssue);
|
|
68
107
|
if (missingReviewFixHeadError) {
|
|
69
108
|
this.failRunAndClear(run, missingReviewFixHeadError, "escalated");
|
|
70
|
-
|
|
71
|
-
|
|
109
|
+
this.syncFailureOutcome({
|
|
110
|
+
run,
|
|
111
|
+
fallbackIssue: freshIssue,
|
|
112
|
+
message: missingReviewFixHeadError,
|
|
72
113
|
level: "error",
|
|
73
|
-
kind: "turn",
|
|
74
|
-
issueKey: freshIssue.issueKey,
|
|
75
|
-
projectId: run.projectId,
|
|
76
|
-
stage: run.runType,
|
|
77
114
|
status: "same_head_review_handoff_blocked",
|
|
78
115
|
summary: missingReviewFixHeadError,
|
|
79
116
|
});
|
|
80
|
-
void this.linearSync.emitActivity(failedIssue, buildRunFailureActivity(run.runType, missingReviewFixHeadError));
|
|
81
|
-
void this.linearSync.syncSession(failedIssue, { activeRunType: run.runType });
|
|
82
|
-
this.linearSync.clearProgress(run.id);
|
|
83
|
-
this.releaseLease(run.projectId, run.linearIssueId);
|
|
84
117
|
return;
|
|
85
118
|
}
|
|
86
119
|
const publishedOutcomeError = await this.completionPolicy.verifyPublishedRunOutcome(run, freshIssue);
|
|
87
120
|
if (publishedOutcomeError) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
detail: publishedOutcomeError,
|
|
97
|
-
});
|
|
98
|
-
void this.linearSync.emitActivity(freshIssue, buildCompletionCheckActivity("started"), { ephemeral: true });
|
|
99
|
-
let completionCheck;
|
|
100
|
-
try {
|
|
101
|
-
completionCheck = await this.completionCheck.run({
|
|
102
|
-
issue: freshIssue,
|
|
103
|
-
run,
|
|
104
|
-
noPrSummary: publishedOutcomeError,
|
|
105
|
-
onStarted: ({ threadId: completionCheckThreadId, turnId: completionCheckTurnId }) => {
|
|
106
|
-
this.db.runs.markCompletionCheckStarted(run.id, {
|
|
107
|
-
threadId: completionCheckThreadId,
|
|
108
|
-
turnId: completionCheckTurnId,
|
|
109
|
-
});
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
catch (error) {
|
|
114
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
115
|
-
this.failRunAndClear(run, `No PR observed and the completion check failed: ${message}`, "failed");
|
|
116
|
-
const failedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? freshIssue;
|
|
117
|
-
this.feed?.publish({
|
|
118
|
-
level: "error",
|
|
119
|
-
kind: "turn",
|
|
120
|
-
issueKey: freshIssue.issueKey,
|
|
121
|
-
projectId: run.projectId,
|
|
122
|
-
stage: run.runType,
|
|
123
|
-
status: "completion_check_failed",
|
|
124
|
-
summary: "No PR found; completion check failed",
|
|
125
|
-
detail: message,
|
|
126
|
-
});
|
|
127
|
-
void this.linearSync.emitActivity(failedIssue, buildRunFailureActivity(run.runType, `No PR observed and the completion check failed: ${message}`));
|
|
128
|
-
void this.linearSync.syncSession(failedIssue, { activeRunType: run.runType });
|
|
129
|
-
this.clearProgressAndRelease(run);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
const completedRunUpdate = this.buildCompletedRunUpdate({
|
|
121
|
+
await handleNoPrCompletionCheck({
|
|
122
|
+
db: this.db,
|
|
123
|
+
logger: this.logger,
|
|
124
|
+
withHeldLease: this.withHeldLease,
|
|
125
|
+
completionCheck: this.completionCheck,
|
|
126
|
+
run,
|
|
127
|
+
issue: freshIssue,
|
|
128
|
+
report,
|
|
133
129
|
threadId,
|
|
134
130
|
...(params.completedTurnId ? { completedTurnId: params.completedTurnId } : {}),
|
|
135
|
-
|
|
131
|
+
publishedOutcomeError,
|
|
132
|
+
failRunAndClear: this.failRunAndClear,
|
|
133
|
+
emitActivity: (issueRecord, activity, options) => this.linearSync.emitActivity(issueRecord, activity, options),
|
|
134
|
+
publishTurnEvent: (event) => this.publishTurnEvent(event),
|
|
135
|
+
syncFailureOutcome: (event) => this.syncFailureOutcome(event),
|
|
136
|
+
syncCompletionCheckOutcome: (event) => this.syncCompletionCheckOutcome(event),
|
|
137
|
+
clearProgressAndRelease: (releaseRun) => this.clearProgressAndRelease(releaseRun),
|
|
136
138
|
});
|
|
137
|
-
if (completionCheck.outcome === "continue") {
|
|
138
|
-
const continued = this.withHeldLease(run.projectId, run.linearIssueId, (lease) => {
|
|
139
|
-
this.db.runs.finishRun(run.id, completedRunUpdate);
|
|
140
|
-
this.db.runs.saveCompletionCheck(run.id, completionCheck);
|
|
141
|
-
this.db.issues.upsertIssue({
|
|
142
|
-
projectId: run.projectId,
|
|
143
|
-
linearIssueId: run.linearIssueId,
|
|
144
|
-
activeRunId: null,
|
|
145
|
-
factoryState: "delegated",
|
|
146
|
-
pendingRunType: null,
|
|
147
|
-
pendingRunContextJson: null,
|
|
148
|
-
});
|
|
149
|
-
return Boolean(this.db.issueSessions.appendIssueSessionEventWithLease(lease, {
|
|
150
|
-
projectId: run.projectId,
|
|
151
|
-
linearIssueId: run.linearIssueId,
|
|
152
|
-
eventType: "completion_check_continue",
|
|
153
|
-
eventJson: JSON.stringify({
|
|
154
|
-
runType: run.runType,
|
|
155
|
-
summary: completionCheck.summary,
|
|
156
|
-
}),
|
|
157
|
-
dedupeKey: `completion_check_continue:${run.id}`,
|
|
158
|
-
}));
|
|
159
|
-
});
|
|
160
|
-
if (!continued) {
|
|
161
|
-
this.logger.warn({ runId: run.id, issueId: run.linearIssueId }, "Skipping completion-check continue writes after losing issue-session lease");
|
|
162
|
-
this.clearProgressAndRelease(run);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
const continuedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? freshIssue;
|
|
166
|
-
this.feed?.publish({
|
|
167
|
-
level: "info",
|
|
168
|
-
kind: "turn",
|
|
169
|
-
issueKey: freshIssue.issueKey,
|
|
170
|
-
projectId: run.projectId,
|
|
171
|
-
stage: run.runType,
|
|
172
|
-
status: "completion_check_continue",
|
|
173
|
-
summary: "No PR found; continuing automatically",
|
|
174
|
-
detail: completionCheck.summary,
|
|
175
|
-
});
|
|
176
|
-
void this.linearSync.emitActivity(continuedIssue, buildCompletionCheckActivity("continue"), { ephemeral: true });
|
|
177
|
-
void this.linearSync.syncSession(continuedIssue);
|
|
178
|
-
this.linearSync.clearProgress(run.id);
|
|
179
|
-
this.enqueueIssue(run.projectId, run.linearIssueId);
|
|
180
|
-
this.releaseLease(run.projectId, run.linearIssueId);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (completionCheck.outcome === "needs_input") {
|
|
184
|
-
const completed = this.withHeldLease(run.projectId, run.linearIssueId, (lease) => {
|
|
185
|
-
this.db.runs.finishRun(run.id, completedRunUpdate);
|
|
186
|
-
this.db.runs.saveCompletionCheck(run.id, completionCheck);
|
|
187
|
-
this.db.issueSessions.clearPendingIssueSessionEventsWithLease(lease);
|
|
188
|
-
this.db.issues.upsertIssue({
|
|
189
|
-
projectId: run.projectId,
|
|
190
|
-
linearIssueId: run.linearIssueId,
|
|
191
|
-
activeRunId: null,
|
|
192
|
-
factoryState: "awaiting_input",
|
|
193
|
-
pendingRunType: null,
|
|
194
|
-
pendingRunContextJson: null,
|
|
195
|
-
});
|
|
196
|
-
return true;
|
|
197
|
-
});
|
|
198
|
-
if (!completed) {
|
|
199
|
-
this.logger.warn({ runId: run.id, issueId: run.linearIssueId }, "Skipping completion-check needs-input writes after losing issue-session lease");
|
|
200
|
-
this.clearProgressAndRelease(run);
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
const awaitingIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? freshIssue;
|
|
204
|
-
this.feed?.publish({
|
|
205
|
-
level: "warn",
|
|
206
|
-
kind: "turn",
|
|
207
|
-
issueKey: freshIssue.issueKey,
|
|
208
|
-
projectId: run.projectId,
|
|
209
|
-
stage: run.runType,
|
|
210
|
-
status: "completion_check_needs_input",
|
|
211
|
-
summary: "No PR found; waiting for answer",
|
|
212
|
-
detail: completionCheck.question ?? completionCheck.summary,
|
|
213
|
-
});
|
|
214
|
-
void this.linearSync.emitActivity(awaitingIssue, buildCompletionCheckActivity("needs_input", completionCheck));
|
|
215
|
-
void this.linearSync.syncSession(awaitingIssue);
|
|
216
|
-
this.clearProgressAndRelease(run);
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
if (completionCheck.outcome === "done") {
|
|
220
|
-
const completed = this.withHeldLease(run.projectId, run.linearIssueId, (lease) => {
|
|
221
|
-
this.db.runs.finishRun(run.id, completedRunUpdate);
|
|
222
|
-
this.db.runs.saveCompletionCheck(run.id, completionCheck);
|
|
223
|
-
this.db.issueSessions.clearPendingIssueSessionEventsWithLease(lease);
|
|
224
|
-
this.db.issues.upsertIssue({
|
|
225
|
-
projectId: run.projectId,
|
|
226
|
-
linearIssueId: run.linearIssueId,
|
|
227
|
-
activeRunId: null,
|
|
228
|
-
factoryState: "done",
|
|
229
|
-
pendingRunType: null,
|
|
230
|
-
pendingRunContextJson: null,
|
|
231
|
-
lastGitHubFailureSource: null,
|
|
232
|
-
lastGitHubFailureHeadSha: null,
|
|
233
|
-
lastGitHubFailureSignature: null,
|
|
234
|
-
lastGitHubFailureCheckName: null,
|
|
235
|
-
lastGitHubFailureCheckUrl: null,
|
|
236
|
-
lastGitHubFailureContextJson: null,
|
|
237
|
-
lastGitHubFailureAt: null,
|
|
238
|
-
lastQueueIncidentJson: null,
|
|
239
|
-
lastAttemptedFailureHeadSha: null,
|
|
240
|
-
lastAttemptedFailureSignature: null,
|
|
241
|
-
});
|
|
242
|
-
return true;
|
|
243
|
-
});
|
|
244
|
-
if (!completed) {
|
|
245
|
-
this.logger.warn({ runId: run.id, issueId: run.linearIssueId }, "Skipping completion-check done writes after losing issue-session lease");
|
|
246
|
-
this.clearProgressAndRelease(run);
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
const doneIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? freshIssue;
|
|
250
|
-
this.feed?.publish({
|
|
251
|
-
level: "info",
|
|
252
|
-
kind: "turn",
|
|
253
|
-
issueKey: freshIssue.issueKey,
|
|
254
|
-
projectId: run.projectId,
|
|
255
|
-
stage: run.runType,
|
|
256
|
-
status: "completion_check_done",
|
|
257
|
-
summary: "No PR found; confirmed done",
|
|
258
|
-
detail: completionCheck.summary,
|
|
259
|
-
});
|
|
260
|
-
void this.linearSync.emitActivity(doneIssue, buildCompletionCheckActivity("done", completionCheck));
|
|
261
|
-
void this.linearSync.syncSession(doneIssue);
|
|
262
|
-
this.clearProgressAndRelease(run);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
const failureReason = `No PR observed and the completion check failed this run: ${completionCheck.summary}`;
|
|
266
|
-
const failed = this.withHeldLease(run.projectId, run.linearIssueId, () => {
|
|
267
|
-
this.db.runs.finishRun(run.id, {
|
|
268
|
-
...completedRunUpdate,
|
|
269
|
-
status: "failed",
|
|
270
|
-
failureReason,
|
|
271
|
-
});
|
|
272
|
-
this.db.runs.saveCompletionCheck(run.id, completionCheck);
|
|
273
|
-
this.db.issues.upsertIssue({
|
|
274
|
-
projectId: run.projectId,
|
|
275
|
-
linearIssueId: run.linearIssueId,
|
|
276
|
-
activeRunId: null,
|
|
277
|
-
factoryState: "failed",
|
|
278
|
-
pendingRunType: null,
|
|
279
|
-
pendingRunContextJson: null,
|
|
280
|
-
});
|
|
281
|
-
return true;
|
|
282
|
-
});
|
|
283
|
-
if (!failed) {
|
|
284
|
-
this.logger.warn({ runId: run.id, issueId: run.linearIssueId }, "Skipping completion-check failed writes after losing issue-session lease");
|
|
285
|
-
this.clearProgressAndRelease(run);
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
const failedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? freshIssue;
|
|
289
|
-
this.feed?.publish({
|
|
290
|
-
level: "warn",
|
|
291
|
-
kind: "turn",
|
|
292
|
-
issueKey: freshIssue.issueKey,
|
|
293
|
-
projectId: run.projectId,
|
|
294
|
-
stage: run.runType,
|
|
295
|
-
status: "completion_check_failed",
|
|
296
|
-
summary: "No PR found; completion check failed",
|
|
297
|
-
detail: completionCheck.summary,
|
|
298
|
-
});
|
|
299
|
-
void this.linearSync.emitActivity(failedIssue, buildRunFailureActivity(run.runType, failureReason));
|
|
300
|
-
void this.linearSync.syncSession(failedIssue, { activeRunType: run.runType });
|
|
301
|
-
this.clearProgressAndRelease(run);
|
|
302
139
|
return;
|
|
303
140
|
}
|
|
304
141
|
const refreshedIssue = await this.completionPolicy.refreshIssueAfterReactivePublish(run, freshIssue);
|
|
@@ -357,17 +194,15 @@ export class RunFinalizer {
|
|
|
357
194
|
});
|
|
358
195
|
this.enqueueIssue(run.projectId, run.linearIssueId);
|
|
359
196
|
}
|
|
360
|
-
this.
|
|
197
|
+
this.publishTurnEvent({
|
|
361
198
|
level: "info",
|
|
362
|
-
|
|
199
|
+
run,
|
|
363
200
|
issueKey: issue.issueKey,
|
|
364
|
-
projectId: run.projectId,
|
|
365
|
-
stage: run.runType,
|
|
366
201
|
status: "completed",
|
|
367
202
|
summary: params.source === "notification"
|
|
368
203
|
? `Turn completed for ${run.runType}`
|
|
369
204
|
: `Reconciliation: ${run.runType} completed${postRunState ? ` -> ${postRunState}` : ""}`,
|
|
370
|
-
|
|
205
|
+
detail: report.assistantMessages.at(-1),
|
|
371
206
|
});
|
|
372
207
|
const updatedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? refreshedIssue;
|
|
373
208
|
const completionSummary = report.assistantMessages.at(-1)?.slice(0, 300) ?? `${run.runType} completed.`;
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -2,7 +2,7 @@ import { summarizeCurrentThread } from "./run-reporting.js";
|
|
|
2
2
|
import { buildRunStartedActivity, } from "./linear-session-reporting.js";
|
|
3
3
|
import { CompletionCheckService } from "./completion-check.js";
|
|
4
4
|
import { WorktreeManager } from "./worktree-manager.js";
|
|
5
|
-
import {
|
|
5
|
+
import { MergedLinearCompletionReconciler } from "./merged-linear-completion-reconciler.js";
|
|
6
6
|
import { QueueHealthMonitor } from "./queue-health-monitor.js";
|
|
7
7
|
import { IdleIssueReconciler, resolveBranchOwnerForStateTransition } from "./idle-reconciliation.js";
|
|
8
8
|
import { LinearSessionSync } from "./linear-session-sync.js";
|
|
@@ -53,6 +53,7 @@ export class RunOrchestrator {
|
|
|
53
53
|
completionCheck;
|
|
54
54
|
runNotificationHandler;
|
|
55
55
|
runReconciler;
|
|
56
|
+
mergedLinearCompletionReconciler;
|
|
56
57
|
activeSessionLeases;
|
|
57
58
|
botIdentity;
|
|
58
59
|
constructor(config, db, codex, linearProvider, enqueueIssue, logger, feed) {
|
|
@@ -79,6 +80,7 @@ export class RunOrchestrator {
|
|
|
79
80
|
this.idleReconciler = new IdleIssueReconciler(db, config, {
|
|
80
81
|
enqueueIssue: (projectId, issueId) => this.enqueueIssue(projectId, issueId),
|
|
81
82
|
}, logger, feed);
|
|
83
|
+
this.mergedLinearCompletionReconciler = new MergedLinearCompletionReconciler(db, linearProvider, logger);
|
|
82
84
|
this.queueHealthMonitor = new QueueHealthMonitor(db, config, {
|
|
83
85
|
advanceIdleIssue: (issue, newState, options) => this.idleReconciler.advanceIdleIssue(issue, newState, options),
|
|
84
86
|
enqueueIssue: (projectId, issueId) => this.enqueueIssue(projectId, issueId),
|
|
@@ -254,44 +256,7 @@ export class RunOrchestrator {
|
|
|
254
256
|
// Advance issues stuck in pr_open whose stored PR metadata already
|
|
255
257
|
// shows they should transition (e.g. approved PR, missed webhook).
|
|
256
258
|
await this.idleReconciler.reconcile();
|
|
257
|
-
await this.
|
|
258
|
-
}
|
|
259
|
-
async reconcileMergedLinearCompletion() {
|
|
260
|
-
for (const issue of this.db.issues.listIssues()) {
|
|
261
|
-
if (issue.prState !== "merged")
|
|
262
|
-
continue;
|
|
263
|
-
if (issue.currentLinearStateType?.trim().toLowerCase() === "completed")
|
|
264
|
-
continue;
|
|
265
|
-
const linear = await this.linearProvider.forProject(issue.projectId).catch(() => undefined);
|
|
266
|
-
if (!linear)
|
|
267
|
-
continue;
|
|
268
|
-
try {
|
|
269
|
-
const liveIssue = await linear.getIssue(issue.linearIssueId);
|
|
270
|
-
const targetState = resolvePreferredCompletedLinearState(liveIssue);
|
|
271
|
-
if (!targetState)
|
|
272
|
-
continue;
|
|
273
|
-
const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
|
|
274
|
-
if (normalizedCurrent === targetState.trim().toLowerCase()) {
|
|
275
|
-
this.db.issues.upsertIssue({
|
|
276
|
-
projectId: issue.projectId,
|
|
277
|
-
linearIssueId: issue.linearIssueId,
|
|
278
|
-
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
279
|
-
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
280
|
-
});
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
const updated = await linear.setIssueState(issue.linearIssueId, targetState);
|
|
284
|
-
this.db.issues.upsertIssue({
|
|
285
|
-
projectId: issue.projectId,
|
|
286
|
-
linearIssueId: issue.linearIssueId,
|
|
287
|
-
...(updated.stateName ? { currentLinearState: updated.stateName } : {}),
|
|
288
|
-
...(updated.stateType ? { currentLinearStateType: updated.stateType } : {}),
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
catch (error) {
|
|
292
|
-
this.logger.warn({ issueKey: issue.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to reconcile merged issue to a completed Linear state");
|
|
293
|
-
}
|
|
294
|
-
}
|
|
259
|
+
await this.mergedLinearCompletionReconciler.reconcile();
|
|
295
260
|
}
|
|
296
261
|
// advanceIdleIssue is now on IdleIssueReconciler — delegate for internal callers
|
|
297
262
|
advanceIdleIssue(issue, newState, options) {
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { buildOperatorRetryEvent } from "./operator-retry-event.js";
|
|
2
|
+
export class ServiceIssueActions {
|
|
3
|
+
db;
|
|
4
|
+
codex;
|
|
5
|
+
runtime;
|
|
6
|
+
feed;
|
|
7
|
+
logger;
|
|
8
|
+
constructor(db, codex, runtime, feed, logger) {
|
|
9
|
+
this.db = db;
|
|
10
|
+
this.codex = codex;
|
|
11
|
+
this.runtime = runtime;
|
|
12
|
+
this.feed = feed;
|
|
13
|
+
this.logger = logger;
|
|
14
|
+
}
|
|
15
|
+
async promptIssue(issueKey, text, source = "watch") {
|
|
16
|
+
const issue = this.db.issues.getIssueByKey(issueKey);
|
|
17
|
+
if (!issue)
|
|
18
|
+
return undefined;
|
|
19
|
+
this.feed.publish({
|
|
20
|
+
level: "info",
|
|
21
|
+
kind: "comment",
|
|
22
|
+
issueKey: issue.issueKey,
|
|
23
|
+
projectId: issue.projectId,
|
|
24
|
+
stage: issue.factoryState,
|
|
25
|
+
status: "operator_prompt",
|
|
26
|
+
summary: `Operator prompt (${source})`,
|
|
27
|
+
detail: text.slice(0, 200),
|
|
28
|
+
});
|
|
29
|
+
if (!issue.activeRunId) {
|
|
30
|
+
this.queueOperatorPrompt(issue, text, source);
|
|
31
|
+
return { delivered: false, queued: true };
|
|
32
|
+
}
|
|
33
|
+
const run = this.db.runs.getRunById(issue.activeRunId);
|
|
34
|
+
if (!run?.threadId || !run.turnId) {
|
|
35
|
+
return { error: "Active run has no thread or turn yet" };
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await this.codex.steerTurn({
|
|
39
|
+
threadId: run.threadId,
|
|
40
|
+
turnId: run.turnId,
|
|
41
|
+
input: `Operator prompt (${source}):\n\n${text}`,
|
|
42
|
+
});
|
|
43
|
+
return { delivered: true };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
47
|
+
this.logger.warn({ issueKey, error: msg }, "steerTurn failed, queuing prompt for next run");
|
|
48
|
+
this.queueOperatorPrompt(issue, text, source);
|
|
49
|
+
return { delivered: false, queued: true };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async stopIssue(issueKey) {
|
|
53
|
+
const issue = this.db.issues.getIssueByKey(issueKey);
|
|
54
|
+
if (!issue)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (!issue.activeRunId)
|
|
57
|
+
return { error: "No active run to stop" };
|
|
58
|
+
const run = this.db.runs.getRunById(issue.activeRunId);
|
|
59
|
+
if (run?.threadId && run.turnId) {
|
|
60
|
+
try {
|
|
61
|
+
await this.codex.steerTurn({
|
|
62
|
+
threadId: run.threadId,
|
|
63
|
+
turnId: run.turnId,
|
|
64
|
+
input: "STOP: The operator has requested this run to halt immediately. Finish your current action, commit any partial progress, and stop.",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Turn may already be done.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
72
|
+
projectId: issue.projectId,
|
|
73
|
+
linearIssueId: issue.linearIssueId,
|
|
74
|
+
eventType: "stop_requested",
|
|
75
|
+
dedupeKey: `operator_stop:${issue.linearIssueId}`,
|
|
76
|
+
});
|
|
77
|
+
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
78
|
+
this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
79
|
+
projectId: issue.projectId,
|
|
80
|
+
linearIssueId: issue.linearIssueId,
|
|
81
|
+
factoryState: "awaiting_input",
|
|
82
|
+
});
|
|
83
|
+
this.feed.publish({
|
|
84
|
+
level: "warn",
|
|
85
|
+
kind: "workflow",
|
|
86
|
+
issueKey: issue.issueKey,
|
|
87
|
+
projectId: issue.projectId,
|
|
88
|
+
status: "stopped",
|
|
89
|
+
summary: "Operator stopped the run",
|
|
90
|
+
});
|
|
91
|
+
return { stopped: true };
|
|
92
|
+
}
|
|
93
|
+
retryIssue(issueKey) {
|
|
94
|
+
const issue = this.db.issues.getIssueByKey(issueKey);
|
|
95
|
+
if (!issue)
|
|
96
|
+
return undefined;
|
|
97
|
+
if (issue.activeRunId)
|
|
98
|
+
return { error: "Issue already has an active run" };
|
|
99
|
+
const issueSession = this.db.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId);
|
|
100
|
+
if (issue.prState === "merged") {
|
|
101
|
+
this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
102
|
+
projectId: issue.projectId,
|
|
103
|
+
linearIssueId: issue.linearIssueId,
|
|
104
|
+
factoryState: "done",
|
|
105
|
+
});
|
|
106
|
+
return { issueKey, runType: "none" };
|
|
107
|
+
}
|
|
108
|
+
let runType = "implementation";
|
|
109
|
+
let factoryState = "delegated";
|
|
110
|
+
if (issue.prNumber && issue.lastGitHubFailureSource === "queue_eviction") {
|
|
111
|
+
runType = "queue_repair";
|
|
112
|
+
factoryState = "repairing_queue";
|
|
113
|
+
}
|
|
114
|
+
else if (issue.prNumber && (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure" || issue.lastGitHubFailureSource === "branch_ci")) {
|
|
115
|
+
runType = "ci_repair";
|
|
116
|
+
factoryState = "repairing_ci";
|
|
117
|
+
}
|
|
118
|
+
else if (issue.prNumber && issue.prReviewState === "changes_requested") {
|
|
119
|
+
runType = issue.pendingRunType === "branch_upkeep" || issueSession?.lastRunType === "branch_upkeep"
|
|
120
|
+
? "branch_upkeep"
|
|
121
|
+
: "review_fix";
|
|
122
|
+
factoryState = "changes_requested";
|
|
123
|
+
}
|
|
124
|
+
else if (issue.prNumber) {
|
|
125
|
+
runType = "implementation";
|
|
126
|
+
factoryState = "implementing";
|
|
127
|
+
}
|
|
128
|
+
this.appendOperatorRetryEvent(issue, runType);
|
|
129
|
+
this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
130
|
+
projectId: issue.projectId,
|
|
131
|
+
linearIssueId: issue.linearIssueId,
|
|
132
|
+
factoryState: factoryState,
|
|
133
|
+
});
|
|
134
|
+
this.feed.publish({
|
|
135
|
+
level: "info",
|
|
136
|
+
kind: "stage",
|
|
137
|
+
issueKey: issue.issueKey,
|
|
138
|
+
projectId: issue.projectId,
|
|
139
|
+
stage: factoryState,
|
|
140
|
+
status: "retry",
|
|
141
|
+
summary: `Retry queued: ${runType}`,
|
|
142
|
+
});
|
|
143
|
+
if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
144
|
+
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
145
|
+
}
|
|
146
|
+
return { issueKey, runType };
|
|
147
|
+
}
|
|
148
|
+
queueOperatorPrompt(issue, text, source) {
|
|
149
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
150
|
+
projectId: issue.projectId,
|
|
151
|
+
linearIssueId: issue.linearIssueId,
|
|
152
|
+
eventType: "operator_prompt",
|
|
153
|
+
eventJson: JSON.stringify({ text, source }),
|
|
154
|
+
});
|
|
155
|
+
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
156
|
+
}
|
|
157
|
+
appendOperatorRetryEvent(issue, runType) {
|
|
158
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
159
|
+
projectId: issue.projectId,
|
|
160
|
+
linearIssueId: issue.linearIssueId,
|
|
161
|
+
...buildOperatorRetryEvent(issue, runType),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|