patchrelay 0.36.18 → 0.37.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/cli/watch/App.js +226 -27
- package/dist/cli/watch/HelpBar.js +18 -9
- package/dist/cli/watch/IssueDetailView.js +32 -14
- package/dist/cli/watch/detail-rows.js +1 -22
- package/dist/cli/watch/detail-status.js +38 -0
- package/dist/cli/watch/layout-measure.js +7 -0
- package/dist/cli/watch/prompt-layout.js +14 -0
- package/dist/cli/watch/timeline-builder.js +169 -18
- package/dist/cli/watch/timeline-presentation.js +21 -1
- package/dist/cli/watch/transient-status.js +28 -0
- package/dist/cli/watch/watch-actions.js +76 -0
- package/dist/cli/watch/watch-state.js +2 -12
- 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/no-pr-completion-check.js +199 -0
- package/dist/operator-retry-event.js +58 -0
- package/dist/run-finalizer.js +72 -237
- 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/cli/watch/ItemLine.js +0 -80
- package/dist/cli/watch/Timeline.js +0 -22
- package/dist/cli/watch/TimelineRow.js +0 -77
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.`;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export class ServiceStartupRecovery {
|
|
2
|
+
db;
|
|
3
|
+
linearProvider;
|
|
4
|
+
linearSync;
|
|
5
|
+
enqueueIssue;
|
|
6
|
+
logger;
|
|
7
|
+
constructor(db, linearProvider, linearSync, enqueueIssue, logger) {
|
|
8
|
+
this.db = db;
|
|
9
|
+
this.linearProvider = linearProvider;
|
|
10
|
+
this.linearSync = linearSync;
|
|
11
|
+
this.enqueueIssue = enqueueIssue;
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
}
|
|
14
|
+
async syncKnownAgentSessions() {
|
|
15
|
+
for (const issue of this.db.issues.listIssues()) {
|
|
16
|
+
if (issue.factoryState === "done") {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const syncedIssue = issue.agentSessionId
|
|
20
|
+
? issue
|
|
21
|
+
: (() => {
|
|
22
|
+
const recoveredAgentSessionId = this.db.webhookEvents.findLatestAgentSessionIdForIssue(issue.linearIssueId);
|
|
23
|
+
return recoveredAgentSessionId
|
|
24
|
+
? this.db.issues.upsertIssue({
|
|
25
|
+
projectId: issue.projectId,
|
|
26
|
+
linearIssueId: issue.linearIssueId,
|
|
27
|
+
agentSessionId: recoveredAgentSessionId,
|
|
28
|
+
})
|
|
29
|
+
: issue;
|
|
30
|
+
})();
|
|
31
|
+
if (!syncedIssue.agentSessionId) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const activeRun = syncedIssue.activeRunId ? this.db.runs.getRunById(syncedIssue.activeRunId) : undefined;
|
|
35
|
+
await this.linearSync.syncSession(syncedIssue, activeRun ? { activeRunType: activeRun.runType } : undefined);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async recoverDelegatedIssueStateFromLinear() {
|
|
39
|
+
for (const issue of this.db.issues.listIssuesWithAgentSessions()) {
|
|
40
|
+
if (issue.factoryState === "done" || issue.activeRunId !== undefined) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const linear = await this.linearProvider.forProject(issue.projectId).catch(() => undefined);
|
|
44
|
+
if (!linear) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const installation = this.db.linearInstallations.getLinearInstallationForProject(issue.projectId);
|
|
48
|
+
if (!installation?.actorId) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const liveIssue = await linear.getIssue(issue.linearIssueId).catch(() => undefined);
|
|
52
|
+
if (!liveIssue) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
this.db.issues.replaceIssueDependencies({
|
|
56
|
+
projectId: issue.projectId,
|
|
57
|
+
linearIssueId: issue.linearIssueId,
|
|
58
|
+
blockers: liveIssue.blockedBy.map((blocker) => ({
|
|
59
|
+
blockerLinearIssueId: blocker.id,
|
|
60
|
+
...(blocker.identifier ? { blockerIssueKey: blocker.identifier } : {}),
|
|
61
|
+
...(blocker.title ? { blockerTitle: blocker.title } : {}),
|
|
62
|
+
...(blocker.stateName ? { blockerCurrentLinearState: blocker.stateName } : {}),
|
|
63
|
+
...(blocker.stateType ? { blockerCurrentLinearStateType: blocker.stateType } : {}),
|
|
64
|
+
})),
|
|
65
|
+
});
|
|
66
|
+
const delegated = liveIssue.delegateId === installation.actorId;
|
|
67
|
+
const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
68
|
+
const shouldRecoverAwaitingInput = delegated
|
|
69
|
+
&& issue.factoryState === "awaiting_input"
|
|
70
|
+
&& this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) === undefined;
|
|
71
|
+
const updated = this.db.issues.upsertIssue({
|
|
72
|
+
projectId: issue.projectId,
|
|
73
|
+
linearIssueId: issue.linearIssueId,
|
|
74
|
+
...(liveIssue.identifier ? { issueKey: liveIssue.identifier } : {}),
|
|
75
|
+
...(liveIssue.title ? { title: liveIssue.title } : {}),
|
|
76
|
+
...(liveIssue.description ? { description: liveIssue.description } : {}),
|
|
77
|
+
...(liveIssue.url ? { url: liveIssue.url } : {}),
|
|
78
|
+
...(liveIssue.priority != null ? { priority: liveIssue.priority } : {}),
|
|
79
|
+
...(liveIssue.estimate != null ? { estimate: liveIssue.estimate } : {}),
|
|
80
|
+
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
81
|
+
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
82
|
+
...(shouldRecoverAwaitingInput ? { factoryState: "delegated" } : {}),
|
|
83
|
+
});
|
|
84
|
+
if (!shouldRecoverAwaitingInput) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (unresolvedBlockers === 0) {
|
|
88
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
89
|
+
projectId: issue.projectId,
|
|
90
|
+
linearIssueId: issue.linearIssueId,
|
|
91
|
+
eventType: "delegated",
|
|
92
|
+
dedupeKey: `delegated:${issue.linearIssueId}`,
|
|
93
|
+
});
|
|
94
|
+
if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
95
|
+
this.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
96
|
+
}
|
|
97
|
+
this.logger.info({ issueKey: updated.issueKey }, "Recovered delegated issue from stale awaiting_input state and re-queued implementation");
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
this.logger.info({ issueKey: updated.issueKey, unresolvedBlockers }, "Recovered delegated blocked issue from stale awaiting_input state");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|