patchrelay 0.75.3 → 0.77.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/agent-input-service.js +40 -26
- package/dist/build-info.json +3 -3
- package/dist/cli/data.js +3 -1
- package/dist/db/issue-session-store.js +44 -9
- package/dist/db/issue-store.js +11 -2
- package/dist/db/migrations.js +3 -0
- package/dist/factory-state.js +23 -0
- package/dist/github-webhook-reactive-run.js +15 -11
- package/dist/github-webhook-stack-coordination.js +8 -4
- package/dist/github-webhook-state-projector.js +204 -139
- package/dist/github-webhook-terminal-handler.js +37 -27
- package/dist/idle-reconciliation.js +122 -66
- package/dist/implementation-outcome-policy.js +5 -1
- package/dist/issue-session-projection-invalidator.js +9 -0
- package/dist/linear-agent-session-client.js +16 -8
- package/dist/linear-issue-projection.js +15 -11
- package/dist/linear-status-comment-sync.js +8 -4
- package/dist/linear-workflow-state-sync.js +9 -5
- package/dist/merged-linear-completion-reconciler.js +39 -17
- package/dist/no-pr-completion-check.js +51 -29
- package/dist/orchestration-parent-wake.js +15 -8
- package/dist/queue-health-monitor.js +17 -8
- package/dist/reactive-run-policy.js +5 -1
- package/dist/run-budgets.js +40 -6
- package/dist/run-completion-policy.js +50 -9
- package/dist/run-failure-policy.js +463 -0
- package/dist/run-finalizer.js +68 -35
- package/dist/run-launcher.js +63 -12
- package/dist/run-notification-handler.js +19 -9
- package/dist/run-orchestrator.js +70 -78
- package/dist/run-reconciler.js +137 -64
- package/dist/run-settlement.js +57 -0
- package/dist/run-wake-planner.js +39 -29
- package/dist/service-issue-actions.js +45 -28
- package/dist/service-startup-recovery.js +61 -35
- package/dist/telemetry.js +9 -0
- package/dist/terminal-wake-reconciler.js +20 -3
- package/dist/webhooks/agent-session-handler.js +22 -12
- package/dist/webhooks/dependency-readiness-handler.js +17 -10
- package/dist/webhooks/desired-stage-recorder.js +32 -13
- package/dist/webhooks/issue-removal-handler.js +24 -13
- package/package.json +1 -1
- package/dist/interrupted-run-recovery.js +0 -227
- package/dist/run-recovery-service.js +0 -202
- package/dist/zombie-recovery.js +0 -13
|
@@ -3,6 +3,7 @@ import { deriveIssueSessionReactiveIntent } from "./issue-session.js";
|
|
|
3
3
|
import { isResumablePausedLocalWork } from "./paused-issue-state.js";
|
|
4
4
|
import { buildRepairWakeDedupeKey, buildRequestedChangesWakeIdentity, reactiveWakeEventType } from "./reactive-wake-keys.js";
|
|
5
5
|
import { upsertLinearIssueProjection } from "./linear-issue-projection.js";
|
|
6
|
+
const WRITER = "service-startup-recovery";
|
|
6
7
|
export class ServiceStartupRecovery {
|
|
7
8
|
config;
|
|
8
9
|
db;
|
|
@@ -30,13 +31,17 @@ export class ServiceStartupRecovery {
|
|
|
30
31
|
? issue
|
|
31
32
|
: (() => {
|
|
32
33
|
const recoveredAgentSessionId = this.db.webhookEvents.findLatestAgentSessionIdForIssue(issue.linearIssueId);
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
if (!recoveredAgentSessionId)
|
|
35
|
+
return issue;
|
|
36
|
+
const commit = this.db.issueSessions.commitIssueState({
|
|
37
|
+
writer: WRITER,
|
|
38
|
+
update: {
|
|
35
39
|
projectId: issue.projectId,
|
|
36
40
|
linearIssueId: issue.linearIssueId,
|
|
37
41
|
agentSessionId: recoveredAgentSessionId,
|
|
38
|
-
}
|
|
39
|
-
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return commit.outcome === "applied" ? commit.issue : issue;
|
|
40
45
|
})();
|
|
41
46
|
if (!syncedIssue.agentSessionId) {
|
|
42
47
|
continue;
|
|
@@ -54,7 +59,7 @@ export class ServiceStartupRecovery {
|
|
|
54
59
|
}
|
|
55
60
|
async recoverDelegatedIssueStateFromLinear() {
|
|
56
61
|
await this.discoverDelegatedIssuesFromLinear();
|
|
57
|
-
for (
|
|
62
|
+
for (let issue of this.db.issues.listIssues()) {
|
|
58
63
|
if (issue.factoryState === "done" || issue.activeRunId !== undefined) {
|
|
59
64
|
continue;
|
|
60
65
|
}
|
|
@@ -71,6 +76,9 @@ export class ServiceStartupRecovery {
|
|
|
71
76
|
continue;
|
|
72
77
|
}
|
|
73
78
|
upsertLinearIssueProjection(this.db, issue.projectId, liveIssue);
|
|
79
|
+
// The projection write bumped the issue version; continue with the
|
|
80
|
+
// fresh row so the recovery commit below doesn't self-conflict.
|
|
81
|
+
issue = this.db.issues.getIssue(issue.projectId, issue.linearIssueId) ?? issue;
|
|
74
82
|
const delegated = liveIssue.delegateId === installation.actorId;
|
|
75
83
|
if (issue.delegatedToPatchRelay !== delegated) {
|
|
76
84
|
appendDelegationObservedEvent(this.db, {
|
|
@@ -116,24 +124,36 @@ export class ServiceStartupRecovery {
|
|
|
116
124
|
const shouldRecoverReactivePrWork = delegated
|
|
117
125
|
&& issue.prNumber !== undefined
|
|
118
126
|
&& reactiveIntent !== undefined;
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
? {
|
|
133
|
-
:
|
|
134
|
-
|
|
135
|
-
:
|
|
127
|
+
const commit = this.db.issueSessions.commitIssueState({
|
|
128
|
+
writer: WRITER,
|
|
129
|
+
expectedVersion: issue.version,
|
|
130
|
+
update: {
|
|
131
|
+
projectId: issue.projectId,
|
|
132
|
+
linearIssueId: issue.linearIssueId,
|
|
133
|
+
delegatedToPatchRelay: delegated,
|
|
134
|
+
...(liveIssue.identifier ? { issueKey: liveIssue.identifier } : {}),
|
|
135
|
+
...(liveIssue.title ? { title: liveIssue.title } : {}),
|
|
136
|
+
...(liveIssue.description ? { description: liveIssue.description } : {}),
|
|
137
|
+
...(liveIssue.url ? { url: liveIssue.url } : {}),
|
|
138
|
+
...(liveIssue.priority != null ? { priority: liveIssue.priority } : {}),
|
|
139
|
+
...(liveIssue.estimate != null ? { estimate: liveIssue.estimate } : {}),
|
|
140
|
+
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
141
|
+
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
142
|
+
...(shouldRecoverPausedLocalWork
|
|
143
|
+
? { factoryState: "delegated" }
|
|
144
|
+
: shouldRecoverReactivePrWork
|
|
145
|
+
? { factoryState: reactiveIntent.compatibilityFactoryState }
|
|
146
|
+
: {}),
|
|
147
|
+
},
|
|
148
|
+
// The recovery decision was derived from the row read at loop start
|
|
149
|
+
// plus stale PR facts; a concurrent writer (webhook, another recovery
|
|
150
|
+
// pass) invalidates it. Skip — reconciliation re-derives shortly.
|
|
151
|
+
onConflict: () => undefined,
|
|
136
152
|
});
|
|
153
|
+
if (commit.outcome !== "applied") {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const updated = commit.issue;
|
|
137
157
|
if (!shouldRecoverPausedLocalWork && !shouldRecoverReactivePrWork) {
|
|
138
158
|
continue;
|
|
139
159
|
}
|
|
@@ -215,20 +235,26 @@ export class ServiceStartupRecovery {
|
|
|
215
235
|
upsertDiscoveredDelegatedIssue(project, liveIssue) {
|
|
216
236
|
upsertLinearIssueProjection(this.db, project.id, liveIssue);
|
|
217
237
|
const existing = this.db.issues.getIssue(project.id, liveIssue.id);
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
238
|
+
const commit = this.db.issueSessions.commitIssueState({
|
|
239
|
+
writer: WRITER,
|
|
240
|
+
update: {
|
|
241
|
+
projectId: project.id,
|
|
242
|
+
linearIssueId: liveIssue.id,
|
|
243
|
+
delegatedToPatchRelay: true,
|
|
244
|
+
factoryState: existing?.factoryState ?? "delegated",
|
|
245
|
+
...(liveIssue.identifier ? { issueKey: liveIssue.identifier } : {}),
|
|
246
|
+
...(liveIssue.title ? { title: liveIssue.title } : {}),
|
|
247
|
+
...(liveIssue.description ? { description: liveIssue.description } : {}),
|
|
248
|
+
...(liveIssue.url ? { url: liveIssue.url } : {}),
|
|
249
|
+
...(liveIssue.priority != null ? { priority: liveIssue.priority } : {}),
|
|
250
|
+
...(liveIssue.estimate != null ? { estimate: liveIssue.estimate } : {}),
|
|
251
|
+
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
252
|
+
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
253
|
+
},
|
|
231
254
|
});
|
|
255
|
+
if (commit.outcome !== "applied")
|
|
256
|
+
return;
|
|
257
|
+
const updated = commit.issue;
|
|
232
258
|
const hasPendingWake = this.db.workflowWakes.peekIssueWake(project.id, liveIssue.id) !== undefined;
|
|
233
259
|
const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(project.id, liveIssue.id);
|
|
234
260
|
if (!hasPendingWake && unresolvedBlockers === 0) {
|
package/dist/telemetry.js
CHANGED
|
@@ -78,6 +78,15 @@ export class OperatorFeedTelemetrySink {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
return undefined;
|
|
81
|
+
case "state.write_conflict":
|
|
82
|
+
return {
|
|
83
|
+
level: "warn",
|
|
84
|
+
kind: "workflow",
|
|
85
|
+
...(event.issueKey ? { issueKey: event.issueKey } : {}),
|
|
86
|
+
...(event.projectId ? { projectId: event.projectId } : {}),
|
|
87
|
+
status: "state_write_conflict",
|
|
88
|
+
summary: `Issue-state write conflict (${event.writer}): expected v${event.expectedVersion ?? "none"}, found v${event.actualVersion ?? "none"} — ${event.resolution.replaceAll("_", " ")}`,
|
|
89
|
+
};
|
|
81
90
|
case "health.invariant":
|
|
82
91
|
return {
|
|
83
92
|
level: event.status === "observed" ? "warn" : "info",
|
|
@@ -15,14 +15,31 @@ export class TerminalWakeReconciler {
|
|
|
15
15
|
&& issue.pendingRunType === undefined) {
|
|
16
16
|
continue;
|
|
17
17
|
}
|
|
18
|
-
this.db.issueSessions.
|
|
19
|
-
|
|
18
|
+
const pendingEvents = this.db.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { pendingOnly: true });
|
|
19
|
+
const clearUpdate = {
|
|
20
20
|
projectId: issue.projectId,
|
|
21
21
|
linearIssueId: issue.linearIssueId,
|
|
22
22
|
pendingRunType: null,
|
|
23
23
|
pendingRunContextJson: null,
|
|
24
|
+
};
|
|
25
|
+
const commit = this.db.issueSessions.commitIssueState({
|
|
26
|
+
writer: "terminal-wake-reconciler",
|
|
27
|
+
expectedVersion: issue.version,
|
|
28
|
+
update: clearUpdate,
|
|
29
|
+
// Only clear if the issue is still terminal on the fresh row.
|
|
30
|
+
onConflict: (current) => (TERMINAL_STATES.has(current.factoryState) ? clearUpdate : undefined),
|
|
24
31
|
});
|
|
25
|
-
|
|
32
|
+
if (commit.outcome !== "applied")
|
|
33
|
+
continue;
|
|
34
|
+
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
35
|
+
// Audit trail: record what was dropped so "why didn't this retry?"
|
|
36
|
+
// is answerable later.
|
|
37
|
+
this.logger.info({
|
|
38
|
+
issueKey: issue.issueKey,
|
|
39
|
+
factoryState: issue.factoryState,
|
|
40
|
+
droppedPendingRunType: issue.pendingRunType,
|
|
41
|
+
droppedEventTypes: pendingEvents.map((event) => event.eventType),
|
|
42
|
+
}, "Reconciliation: cleared stale terminal wake");
|
|
26
43
|
}
|
|
27
44
|
}
|
|
28
45
|
}
|
|
@@ -3,6 +3,7 @@ import { buildAgentSessionExternalUrls } from "../agent-session-presentation.js"
|
|
|
3
3
|
import { buildAlreadyRunningThought, buildAgentSessionAcknowledgementThought, buildBlockedDelegationActivity, buildDelegationThought, buildStopConfirmationActivity, } from "../linear-session-reporting.js";
|
|
4
4
|
import { dirtyWorktreeEventPayload, inspectGitWorktreeStatus } from "../git-worktree-status.js";
|
|
5
5
|
import { resolveProject, triggerEventAllowed } from "../project-resolution.js";
|
|
6
|
+
const WRITER = "agent-session-handler";
|
|
6
7
|
const PATCHRELAY_AGENT_ACTIVITY_TYPES = new Set([
|
|
7
8
|
"action",
|
|
8
9
|
"elicitation",
|
|
@@ -172,19 +173,28 @@ export class AgentSessionHandler {
|
|
|
172
173
|
catch (error) {
|
|
173
174
|
this.logger.warn({ issueKey: params.trackedIssue?.issueKey, error: error instanceof Error ? error.message : String(error) }, "Failed to steer Codex turn for stop signal");
|
|
174
175
|
}
|
|
175
|
-
this.db.runs.finishRun(params.activeRun.id, {
|
|
176
|
-
status: "released",
|
|
177
|
-
threadId: params.activeRun.threadId,
|
|
178
|
-
turnId: params.activeRun.turnId,
|
|
179
|
-
failureReason: dirtySummary ? `Stop signal received; ${dirtySummary}` : "Stop signal received",
|
|
180
|
-
});
|
|
181
176
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
177
|
+
// The stop signal is a user fact: the issue slot clear and the run
|
|
178
|
+
// release ride in one transaction, with the run gated on the issue commit.
|
|
179
|
+
this.db.transaction(() => {
|
|
180
|
+
const commit = this.db.issueSessions.commitIssueState({
|
|
181
|
+
writer: WRITER,
|
|
182
|
+
update: {
|
|
183
|
+
projectId: params.project.id,
|
|
184
|
+
linearIssueId: issueId,
|
|
185
|
+
activeRunId: null,
|
|
186
|
+
factoryState: "awaiting_input",
|
|
187
|
+
agentSessionId: sessionId,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
if (commit.outcome === "applied" && params.activeRun?.threadId && params.activeRun.turnId) {
|
|
191
|
+
this.db.runs.finishRun(params.activeRun.id, {
|
|
192
|
+
status: "released",
|
|
193
|
+
threadId: params.activeRun.threadId,
|
|
194
|
+
turnId: params.activeRun.turnId,
|
|
195
|
+
failureReason: dirtySummary ? `Stop signal received; ${dirtySummary}` : "Stop signal received",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
188
198
|
});
|
|
189
199
|
this.db.issueSessions.appendIssueSessionEvent({
|
|
190
200
|
projectId: params.project.id,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { emitTelemetry, noopTelemetry } from "../telemetry.js";
|
|
2
|
+
const WRITER = "dependency-readiness-handler";
|
|
2
3
|
export class DependencyReadinessHandler {
|
|
3
4
|
db;
|
|
4
5
|
wakeDispatcher;
|
|
@@ -41,11 +42,14 @@ export class DependencyReadinessHandler {
|
|
|
41
42
|
if (this.peekPendingSessionWakeRunType(projectId, dependent.linearIssueId) === "implementation"
|
|
42
43
|
&& issue.activeRunId === undefined
|
|
43
44
|
&& !this.db.issueSessions.hasPendingIssueSessionEvents(projectId, dependent.linearIssueId)) {
|
|
44
|
-
this.db.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
this.db.issueSessions.commitIssueState({
|
|
46
|
+
writer: WRITER,
|
|
47
|
+
update: {
|
|
48
|
+
projectId,
|
|
49
|
+
linearIssueId: dependent.linearIssueId,
|
|
50
|
+
pendingRunType: null,
|
|
51
|
+
pendingRunContextJson: null,
|
|
52
|
+
},
|
|
49
53
|
});
|
|
50
54
|
}
|
|
51
55
|
continue;
|
|
@@ -72,11 +76,14 @@ export class DependencyReadinessHandler {
|
|
|
72
76
|
continue;
|
|
73
77
|
}
|
|
74
78
|
if (this.peekPendingSessionWakeRunType(projectId, dependent.linearIssueId) === "implementation") {
|
|
75
|
-
this.db.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
this.db.issueSessions.commitIssueState({
|
|
80
|
+
writer: WRITER,
|
|
81
|
+
update: {
|
|
82
|
+
projectId,
|
|
83
|
+
linearIssueId: dependent.linearIssueId,
|
|
84
|
+
pendingRunType: null,
|
|
85
|
+
pendingRunContextJson: null,
|
|
86
|
+
},
|
|
80
87
|
});
|
|
81
88
|
}
|
|
82
89
|
const dispatchedRunType = this.wakeDispatcher.recordEventAndDispatch(projectId, dependent.linearIssueId, {
|
|
@@ -7,6 +7,7 @@ import { resolveLinkedPrAdoption } from "./linked-pr-adoption.js";
|
|
|
7
7
|
import { buildOperatorRetryEvent } from "../operator-retry-event.js";
|
|
8
8
|
import { planIssueWebhookWorkflow } from "./issue-webhook-workflow-planner.js";
|
|
9
9
|
import { dirtyWorktreeEventPayload, inspectGitWorktreeStatus } from "../git-worktree-status.js";
|
|
10
|
+
const WRITER = "desired-stage-recorder";
|
|
10
11
|
export class DesiredStageRecorder {
|
|
11
12
|
db;
|
|
12
13
|
linearProvider;
|
|
@@ -84,8 +85,14 @@ export class DesiredStageRecorder {
|
|
|
84
85
|
: workflowPlan.effectiveRunRelease.reason
|
|
85
86
|
: undefined;
|
|
86
87
|
const dirtyWorktreePayload = releaseWorktreeStatus ? dirtyWorktreeEventPayload(releaseWorktreeStatus) : undefined;
|
|
87
|
-
const
|
|
88
|
-
|
|
88
|
+
const activeLease = this.db.issueSessions.getActiveIssueSessionLease(params.project.id, normalizedIssue.id);
|
|
89
|
+
// Webhook intake projection: the fields are facts carried by the webhook
|
|
90
|
+
// payload and the hydrated Linear issue, applied unconditionally (the
|
|
91
|
+
// active lease still gates the write, matching the previous semantics).
|
|
92
|
+
const issueCommit = this.db.issueSessions.commitIssueState({
|
|
93
|
+
writer: WRITER,
|
|
94
|
+
...(activeLease ? { lease: activeLease } : {}),
|
|
95
|
+
update: {
|
|
89
96
|
projectId: params.project.id,
|
|
90
97
|
linearIssueId: normalizedIssue.id,
|
|
91
98
|
...(hydratedIssue.identifier ? { issueKey: hydratedIssue.identifier } : {}),
|
|
@@ -103,20 +110,32 @@ export class DesiredStageRecorder {
|
|
|
103
110
|
...linkedPrAdoption?.issueUpdates,
|
|
104
111
|
delegatedToPatchRelay: delegated,
|
|
105
112
|
...workflowPlan.resolvedIssueUpdate,
|
|
106
|
-
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
let issue;
|
|
116
|
+
if (issueCommit.outcome === "applied") {
|
|
117
|
+
issue = issueCommit.issue;
|
|
107
118
|
if (workflowPlan.effectiveRunRelease.release && activeRun && releaseReason) {
|
|
108
119
|
this.db.runs.finishRun(activeRun.id, { status: "released", failureReason: releaseReason });
|
|
109
120
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
}
|
|
122
|
+
else if (existingIssue) {
|
|
123
|
+
issue = existingIssue;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const fallbackCommit = this.db.issueSessions.commitIssueState({
|
|
127
|
+
writer: WRITER,
|
|
128
|
+
update: {
|
|
129
|
+
projectId: params.project.id,
|
|
130
|
+
linearIssueId: normalizedIssue.id,
|
|
131
|
+
...(hydratedIssue.identifier ? { issueKey: hydratedIssue.identifier } : {}),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
if (fallbackCommit.outcome !== "applied") {
|
|
135
|
+
return { issue: undefined, wakeRunType: undefined, delegated };
|
|
136
|
+
}
|
|
137
|
+
issue = fallbackCommit.issue;
|
|
138
|
+
}
|
|
120
139
|
const previousParentIssueId = existingIssue?.parentLinearIssueId;
|
|
121
140
|
const currentParentIssueId = issue.parentLinearIssueId;
|
|
122
141
|
const wasResolved = isResolvedLinearState(existingIssue?.currentLinearStateType, existingIssue?.currentLinearState);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TERMINAL_STATES } from "../factory-state.js";
|
|
2
|
+
const WRITER = "issue-removal-handler";
|
|
2
3
|
export class IssueRemovalHandler {
|
|
3
4
|
db;
|
|
4
5
|
feed;
|
|
@@ -13,27 +14,42 @@ export class IssueRemovalHandler {
|
|
|
13
14
|
const activeLease = this.db.issueSessions.getActiveIssueSessionLease(params.projectId, params.issue.id);
|
|
14
15
|
const commitRemoval = () => {
|
|
15
16
|
if (removedIssue?.activeRunId) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
return this.db.issues.upsertIssue({
|
|
17
|
+
const removedRunId = removedIssue.activeRunId;
|
|
18
|
+
const run = this.db.runs.getRunById(removedRunId);
|
|
19
|
+
const update = {
|
|
21
20
|
projectId: params.projectId,
|
|
22
21
|
linearIssueId: params.issue.id,
|
|
23
22
|
activeRunId: null,
|
|
24
23
|
pendingRunType: null,
|
|
25
24
|
factoryState: "failed",
|
|
25
|
+
};
|
|
26
|
+
const commit = this.db.issueSessions.commitIssueState({
|
|
27
|
+
writer: WRITER,
|
|
28
|
+
expectedVersion: removedIssue.version,
|
|
29
|
+
...(activeLease ? { lease: activeLease } : {}),
|
|
30
|
+
update,
|
|
31
|
+
onConflict: (current) => (current.activeRunId === removedRunId ? update : undefined),
|
|
26
32
|
});
|
|
33
|
+
if (run && commit.outcome === "applied") {
|
|
34
|
+
this.db.runs.finishRun(run.id, { status: "released", failureReason: "Issue removed from Linear" });
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
27
37
|
}
|
|
28
38
|
if (removedIssue && !TERMINAL_STATES.has(removedIssue.factoryState)) {
|
|
29
|
-
|
|
39
|
+
const update = {
|
|
30
40
|
projectId: params.projectId,
|
|
31
41
|
linearIssueId: params.issue.id,
|
|
32
42
|
pendingRunType: null,
|
|
33
43
|
factoryState: "failed",
|
|
44
|
+
};
|
|
45
|
+
this.db.issueSessions.commitIssueState({
|
|
46
|
+
writer: WRITER,
|
|
47
|
+
expectedVersion: removedIssue.version,
|
|
48
|
+
...(activeLease ? { lease: activeLease } : {}),
|
|
49
|
+
update,
|
|
50
|
+
onConflict: (current) => (TERMINAL_STATES.has(current.factoryState) ? undefined : update),
|
|
34
51
|
});
|
|
35
52
|
}
|
|
36
|
-
return removedIssue;
|
|
37
53
|
};
|
|
38
54
|
if (removedIssue?.activeRunId) {
|
|
39
55
|
const run = this.db.runs.getRunById(removedIssue.activeRunId);
|
|
@@ -41,12 +57,7 @@ export class IssueRemovalHandler {
|
|
|
41
57
|
await params.stopActiveRun(run, "STOP: The Linear issue was removed. Stop working immediately and exit.");
|
|
42
58
|
}
|
|
43
59
|
}
|
|
44
|
-
|
|
45
|
-
this.db.issueSessions.withIssueSessionLease(params.projectId, params.issue.id, activeLease.leaseId, commitRemoval);
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
commitRemoval();
|
|
49
|
-
}
|
|
60
|
+
commitRemoval();
|
|
50
61
|
this.db.issueSessions.appendIssueSessionEvent({
|
|
51
62
|
projectId: params.projectId,
|
|
52
63
|
linearIssueId: params.issue.id,
|