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.
@@ -1,5 +1,6 @@
1
1
  import { buildStageReport, countEventMethods } from "./run-reporting.js";
2
- import { buildCompletionCheckActivity, buildRunCompletedActivity, buildRunFailureActivity } from "./linear-session-reporting.js";
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
- const heldIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? freshIssue;
52
- this.feed?.publish({
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
- const failedIssue = this.db.issues.getIssue(run.projectId, run.linearIssueId) ?? freshIssue;
71
- this.feed?.publish({
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
- this.feed?.publish({
89
- level: "info",
90
- kind: "turn",
91
- issueKey: freshIssue.issueKey,
92
- projectId: run.projectId,
93
- stage: run.runType,
94
- status: "completion_check_started",
95
- summary: "No PR found; checking next step",
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
- report,
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.feed?.publish({
197
+ this.publishTurnEvent({
361
198
  level: "info",
362
- kind: "turn",
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
- ...(report.assistantMessages.at(-1) ? { detail: report.assistantMessages.at(-1) } : {}),
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.`;
@@ -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 { resolvePreferredCompletedLinearState } from "./linear-workflow.js";
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.reconcileMergedLinearCompletion();
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
+ }