patchrelay 0.36.7 → 0.36.9
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/cluster-health.js +2 -2
- package/dist/cli/data.js +20 -20
- package/dist/db/issue-session-store.js +284 -0
- package/dist/db/issue-store.js +559 -0
- package/dist/db/run-store.js +125 -0
- package/dist/db/webhook-event-store.js +71 -0
- package/dist/db.js +52 -1138
- package/dist/github-webhook-handler.js +44 -44
- package/dist/idle-reconciliation.js +20 -20
- package/dist/interrupted-run-recovery.js +176 -0
- package/dist/issue-query-service.js +13 -13
- package/dist/issue-session-lease-service.js +143 -0
- package/dist/issue-session-projector.js +114 -0
- package/dist/linear-session-sync.js +10 -10
- package/dist/queue-health-monitor.js +5 -5
- package/dist/run-completion-policy.js +412 -0
- package/dist/run-finalizer.js +172 -0
- package/dist/run-launcher.js +193 -0
- package/dist/run-orchestrator.js +145 -1505
- package/dist/run-recovery-service.js +209 -0
- package/dist/run-wake-planner.js +101 -0
- package/dist/service.js +33 -33
- package/dist/tracked-issue-projector.js +69 -0
- package/dist/webhook-handler.js +64 -693
- package/dist/webhooks/agent-session-handler.js +212 -0
- package/dist/webhooks/comment-policy.js +41 -0
- package/dist/webhooks/comment-wake-handler.js +133 -0
- package/dist/webhooks/decision-helpers.js +74 -0
- package/dist/webhooks/desired-stage-recorder.js +177 -0
- package/dist/webhooks/issue-removal-handler.js +68 -0
- package/dist/worktree-manager.js +69 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -20,7 +20,7 @@ export async function collectClusterHealth(config, db, runCommand) {
|
|
|
20
20
|
});
|
|
21
21
|
const snapshots = openIssues.map((issue) => {
|
|
22
22
|
const tracked = db.getTrackedIssue(issue.projectId, issue.linearIssueId);
|
|
23
|
-
const deps = db.listIssueDependencies(issue.projectId, issue.linearIssueId);
|
|
23
|
+
const deps = db.issues.listIssueDependencies(issue.projectId, issue.linearIssueId);
|
|
24
24
|
const blockedBy = deps.filter((dep) => !isResolvedDependency(dep));
|
|
25
25
|
const missingTrackedBlockers = blockedBy.filter((dep) => {
|
|
26
26
|
if (trackedByLinearId.has(dep.blockerLinearIssueId))
|
|
@@ -31,7 +31,7 @@ export async function collectClusterHealth(config, db, runCommand) {
|
|
|
31
31
|
});
|
|
32
32
|
return {
|
|
33
33
|
issue,
|
|
34
|
-
session: db.getIssueSession(issue.projectId, issue.linearIssueId),
|
|
34
|
+
session: db.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId),
|
|
35
35
|
blockedBy,
|
|
36
36
|
missingTrackedBlockers,
|
|
37
37
|
ageMs: Math.max(0, now - Date.parse(issue.updatedAt || new Date(0).toISOString())),
|
package/dist/cli/data.js
CHANGED
|
@@ -42,7 +42,7 @@ function summarizeThread(thread, latestTimestampSeen) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
function latestEventTimestamp(db, runId) {
|
|
45
|
-
const events = db.listThreadEvents(runId);
|
|
45
|
+
const events = db.runs.listThreadEvents(runId);
|
|
46
46
|
return events.at(-1)?.createdAt;
|
|
47
47
|
}
|
|
48
48
|
function parseObjectJson(value) {
|
|
@@ -94,9 +94,9 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
94
94
|
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
95
95
|
if (!issue)
|
|
96
96
|
return undefined;
|
|
97
|
-
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
98
|
-
const activeRun = dbIssue.activeRunId ? this.db.
|
|
99
|
-
const latestRun = this.db.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
97
|
+
const dbIssue = this.db.issues.getIssueByKey(issueKey);
|
|
98
|
+
const activeRun = dbIssue.activeRunId ? this.db.runs.getRunById(dbIssue.activeRunId) : undefined;
|
|
99
|
+
const latestRun = this.db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
100
100
|
const latestReport = normalizeStageReport(latestRun?.reportJson, latestRun?.status);
|
|
101
101
|
const latestSummary = safeJsonParse(latestRun?.summaryJson);
|
|
102
102
|
const statusNote = latestReport?.assistantMessages.at(-1) ??
|
|
@@ -120,8 +120,8 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
120
120
|
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
121
121
|
if (!issue)
|
|
122
122
|
return undefined;
|
|
123
|
-
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
124
|
-
const run = dbIssue.activeRunId ? this.db.
|
|
123
|
+
const dbIssue = this.db.issues.getIssueByKey(issueKey);
|
|
124
|
+
const run = dbIssue.activeRunId ? this.db.runs.getRunById(dbIssue.activeRunId) : undefined;
|
|
125
125
|
if (!run)
|
|
126
126
|
return undefined;
|
|
127
127
|
const live = run.threadId &&
|
|
@@ -132,7 +132,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
132
132
|
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
133
133
|
if (!issue)
|
|
134
134
|
return undefined;
|
|
135
|
-
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
135
|
+
const dbIssue = this.db.issues.getIssueByKey(issueKey);
|
|
136
136
|
if (!dbIssue.branchName || !dbIssue.worktreePath)
|
|
137
137
|
return undefined;
|
|
138
138
|
return { issue, branchName: dbIssue.branchName, worktreePath: dbIssue.worktreePath, repoId: issue.projectId };
|
|
@@ -141,7 +141,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
141
141
|
const worktree = this.worktree(issueKey);
|
|
142
142
|
if (!worktree)
|
|
143
143
|
return undefined;
|
|
144
|
-
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
144
|
+
const dbIssue = this.db.issues.getIssueByKey(issueKey);
|
|
145
145
|
const resumeThreadId = dbIssue.threadId ?? undefined;
|
|
146
146
|
return {
|
|
147
147
|
...worktree,
|
|
@@ -155,7 +155,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
155
155
|
if (options?.ensureWorktree) {
|
|
156
156
|
await this.ensureOpenWorktree(worktree);
|
|
157
157
|
}
|
|
158
|
-
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
158
|
+
const dbIssue = this.db.issues.getIssueByKey(issueKey);
|
|
159
159
|
const existingThreadId = dbIssue.threadId;
|
|
160
160
|
if (existingThreadId && (await this.canReadThread(existingThreadId))) {
|
|
161
161
|
return { ...worktree, resumeThreadId: existingThreadId };
|
|
@@ -165,7 +165,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
165
165
|
}
|
|
166
166
|
const codex = await this.getCodex();
|
|
167
167
|
const thread = await codex.startThread({ cwd: worktree.worktreePath });
|
|
168
|
-
this.db.upsertIssue({
|
|
168
|
+
this.db.issues.upsertIssue({
|
|
169
169
|
projectId: worktree.issue.projectId,
|
|
170
170
|
linearIssueId: worktree.issue.linearIssueId,
|
|
171
171
|
threadId: thread.id,
|
|
@@ -179,8 +179,8 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
179
179
|
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
180
180
|
if (!issue)
|
|
181
181
|
return undefined;
|
|
182
|
-
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
183
|
-
const issueSession = this.db.getIssueSession(issue.projectId, issue.linearIssueId);
|
|
182
|
+
const dbIssue = this.db.issues.getIssueByKey(issueKey);
|
|
183
|
+
const issueSession = this.db.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId);
|
|
184
184
|
if (dbIssue.activeRunId !== undefined) {
|
|
185
185
|
throw new Error(`Issue ${issueKey} already has an active run.`);
|
|
186
186
|
}
|
|
@@ -200,7 +200,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
200
200
|
? "changes_requested"
|
|
201
201
|
: "delegated";
|
|
202
202
|
this.appendRetryWake(dbIssue, runType);
|
|
203
|
-
this.db.upsertIssue({
|
|
203
|
+
this.db.issues.upsertIssue({
|
|
204
204
|
projectId: issue.projectId,
|
|
205
205
|
linearIssueId: issue.linearIssueId,
|
|
206
206
|
pendingRunType: null,
|
|
@@ -214,8 +214,8 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
214
214
|
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
215
215
|
if (!issue)
|
|
216
216
|
return undefined;
|
|
217
|
-
const dbIssue = this.db.getIssueByKey(issueKey);
|
|
218
|
-
const runs = this.db.listRunsForIssue(issue.projectId, issue.linearIssueId);
|
|
217
|
+
const dbIssue = this.db.issues.getIssueByKey(issueKey);
|
|
218
|
+
const runs = this.db.runs.listRunsForIssue(issue.projectId, issue.linearIssueId);
|
|
219
219
|
const sessions = runs
|
|
220
220
|
.slice()
|
|
221
221
|
.reverse()
|
|
@@ -230,7 +230,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
230
230
|
...(run.parentThreadId ? { parentThreadId: run.parentThreadId } : {}),
|
|
231
231
|
...(summary ? { summary } : {}),
|
|
232
232
|
...(run.failureReason ? { failureReason: run.failureReason } : {}),
|
|
233
|
-
eventCount: this.db.listThreadEvents(run.id).length,
|
|
233
|
+
eventCount: this.db.runs.listThreadEvents(run.id).length,
|
|
234
234
|
startedAt: run.startedAt,
|
|
235
235
|
...(run.endedAt ? { endedAt: run.endedAt } : {}),
|
|
236
236
|
isCurrentThread: run.threadId !== undefined && run.threadId === dbIssue.threadId,
|
|
@@ -247,7 +247,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
247
247
|
if (runType === "queue_repair") {
|
|
248
248
|
const queueIncident = parseObjectJson(issue.lastQueueIncidentJson);
|
|
249
249
|
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson);
|
|
250
|
-
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
250
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
251
251
|
projectId: issue.projectId,
|
|
252
252
|
linearIssueId: issue.linearIssueId,
|
|
253
253
|
eventType: "merge_steward_incident",
|
|
@@ -262,7 +262,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
262
262
|
}
|
|
263
263
|
if (runType === "ci_repair") {
|
|
264
264
|
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson);
|
|
265
|
-
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
265
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
266
266
|
projectId: issue.projectId,
|
|
267
267
|
linearIssueId: issue.linearIssueId,
|
|
268
268
|
eventType: "settled_red_ci",
|
|
@@ -275,7 +275,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
275
275
|
return;
|
|
276
276
|
}
|
|
277
277
|
if (runType === "review_fix" || runType === "branch_upkeep") {
|
|
278
|
-
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
278
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
279
279
|
projectId: issue.projectId,
|
|
280
280
|
linearIssueId: issue.linearIssueId,
|
|
281
281
|
eventType: "review_changes_requested",
|
|
@@ -290,7 +290,7 @@ export class CliDataAccess extends CliOperatorApiClient {
|
|
|
290
290
|
});
|
|
291
291
|
return;
|
|
292
292
|
}
|
|
293
|
-
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
293
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
294
294
|
projectId: issue.projectId,
|
|
295
295
|
linearIssueId: issue.linearIssueId,
|
|
296
296
|
eventType: "delegated",
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { deriveSessionWakePlan } from "../issue-session-events.js";
|
|
2
|
+
import { isoNow } from "./shared.js";
|
|
3
|
+
export class IssueSessionStore {
|
|
4
|
+
connection;
|
|
5
|
+
mapIssueSessionRow;
|
|
6
|
+
mapIssueSessionEventRow;
|
|
7
|
+
issues;
|
|
8
|
+
runs;
|
|
9
|
+
deriveImplicitReactiveWake;
|
|
10
|
+
constructor(connection, mapIssueSessionRow, mapIssueSessionEventRow, issues, runs, deriveImplicitReactiveWake) {
|
|
11
|
+
this.connection = connection;
|
|
12
|
+
this.mapIssueSessionRow = mapIssueSessionRow;
|
|
13
|
+
this.mapIssueSessionEventRow = mapIssueSessionEventRow;
|
|
14
|
+
this.issues = issues;
|
|
15
|
+
this.runs = runs;
|
|
16
|
+
this.deriveImplicitReactiveWake = deriveImplicitReactiveWake;
|
|
17
|
+
}
|
|
18
|
+
getIssueSession(projectId, linearIssueId) {
|
|
19
|
+
const row = this.connection
|
|
20
|
+
.prepare("SELECT * FROM issue_sessions WHERE project_id = ? AND linear_issue_id = ?")
|
|
21
|
+
.get(projectId, linearIssueId);
|
|
22
|
+
return row ? this.mapIssueSessionRow(row) : undefined;
|
|
23
|
+
}
|
|
24
|
+
getIssueSessionByKey(issueKey) {
|
|
25
|
+
const row = this.connection.prepare("SELECT * FROM issue_sessions WHERE issue_key = ?").get(issueKey);
|
|
26
|
+
return row ? this.mapIssueSessionRow(row) : undefined;
|
|
27
|
+
}
|
|
28
|
+
appendIssueSessionEvent(params) {
|
|
29
|
+
if (params.dedupeKey) {
|
|
30
|
+
const existing = this.connection.prepare(`
|
|
31
|
+
SELECT * FROM issue_session_events
|
|
32
|
+
WHERE project_id = ? AND linear_issue_id = ? AND dedupe_key = ? AND processed_at IS NULL
|
|
33
|
+
ORDER BY id DESC LIMIT 1
|
|
34
|
+
`).get(params.projectId, params.linearIssueId, params.dedupeKey);
|
|
35
|
+
if (existing)
|
|
36
|
+
return this.mapIssueSessionEventRow(existing);
|
|
37
|
+
}
|
|
38
|
+
const now = isoNow();
|
|
39
|
+
const result = this.connection.prepare(`
|
|
40
|
+
INSERT INTO issue_session_events (
|
|
41
|
+
project_id, linear_issue_id, event_type, event_json, dedupe_key, created_at
|
|
42
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
43
|
+
`).run(params.projectId, params.linearIssueId, params.eventType, params.eventJson ?? null, params.dedupeKey ?? null, now);
|
|
44
|
+
return this.getIssueSessionEvent(Number(result.lastInsertRowid));
|
|
45
|
+
}
|
|
46
|
+
appendIssueSessionEventWithLease(lease, params) {
|
|
47
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => this.appendIssueSessionEvent(params));
|
|
48
|
+
}
|
|
49
|
+
appendIssueSessionEventRespectingActiveLease(projectId, linearIssueId, params) {
|
|
50
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
51
|
+
if (!lease) {
|
|
52
|
+
return this.appendIssueSessionEvent(params);
|
|
53
|
+
}
|
|
54
|
+
return this.appendIssueSessionEventWithLease(lease, params);
|
|
55
|
+
}
|
|
56
|
+
getIssueSessionEvent(id) {
|
|
57
|
+
const row = this.connection.prepare("SELECT * FROM issue_session_events WHERE id = ?").get(id);
|
|
58
|
+
return row ? this.mapIssueSessionEventRow(row) : undefined;
|
|
59
|
+
}
|
|
60
|
+
listIssueSessionEvents(projectId, linearIssueId, options) {
|
|
61
|
+
const conditions = ["project_id = ?", "linear_issue_id = ?"];
|
|
62
|
+
const values = [projectId, linearIssueId];
|
|
63
|
+
if (options?.pendingOnly) {
|
|
64
|
+
conditions.push("processed_at IS NULL");
|
|
65
|
+
}
|
|
66
|
+
let query = `SELECT * FROM issue_session_events WHERE ${conditions.join(" AND ")} ORDER BY id`;
|
|
67
|
+
if (options?.limit !== undefined) {
|
|
68
|
+
query += " LIMIT ?";
|
|
69
|
+
values.push(options.limit);
|
|
70
|
+
}
|
|
71
|
+
const rows = this.connection.prepare(query).all(...values);
|
|
72
|
+
return rows.map(this.mapIssueSessionEventRow);
|
|
73
|
+
}
|
|
74
|
+
consumeIssueSessionEvents(projectId, linearIssueId, eventIds, runId) {
|
|
75
|
+
if (eventIds.length === 0)
|
|
76
|
+
return;
|
|
77
|
+
const now = isoNow();
|
|
78
|
+
const placeholders = eventIds.map(() => "?").join(", ");
|
|
79
|
+
this.connection.prepare(`
|
|
80
|
+
UPDATE issue_session_events
|
|
81
|
+
SET processed_at = ?, consumed_by_run_id = ?
|
|
82
|
+
WHERE project_id = ? AND linear_issue_id = ? AND id IN (${placeholders}) AND processed_at IS NULL
|
|
83
|
+
`).run(now, runId, projectId, linearIssueId, ...eventIds);
|
|
84
|
+
}
|
|
85
|
+
clearPendingIssueSessionEvents(projectId, linearIssueId) {
|
|
86
|
+
this.connection.prepare(`
|
|
87
|
+
UPDATE issue_session_events
|
|
88
|
+
SET processed_at = ?, consumed_by_run_id = NULL
|
|
89
|
+
WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
|
|
90
|
+
`).run(isoNow(), projectId, linearIssueId);
|
|
91
|
+
}
|
|
92
|
+
hasPendingIssueSessionEvents(projectId, linearIssueId) {
|
|
93
|
+
const row = this.connection.prepare(`
|
|
94
|
+
SELECT 1
|
|
95
|
+
FROM issue_session_events
|
|
96
|
+
WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
|
|
97
|
+
LIMIT 1
|
|
98
|
+
`).get(projectId, linearIssueId);
|
|
99
|
+
return row !== undefined;
|
|
100
|
+
}
|
|
101
|
+
peekIssueSessionWake(projectId, linearIssueId) {
|
|
102
|
+
const issue = this.issues.getIssue(projectId, linearIssueId);
|
|
103
|
+
if (!issue)
|
|
104
|
+
return undefined;
|
|
105
|
+
const events = this.listIssueSessionEvents(projectId, linearIssueId, { pendingOnly: true });
|
|
106
|
+
const plan = deriveSessionWakePlan(issue, events);
|
|
107
|
+
if (plan?.runType) {
|
|
108
|
+
return {
|
|
109
|
+
eventIds: events.map((event) => event.id),
|
|
110
|
+
runType: plan.runType,
|
|
111
|
+
context: plan.context,
|
|
112
|
+
...(plan.wakeReason ? { wakeReason: plan.wakeReason } : {}),
|
|
113
|
+
resumeThread: plan.resumeThread,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const implicitWake = this.deriveImplicitReactiveWake(issue);
|
|
117
|
+
if (!implicitWake)
|
|
118
|
+
return undefined;
|
|
119
|
+
return {
|
|
120
|
+
eventIds: [],
|
|
121
|
+
runType: implicitWake.runType,
|
|
122
|
+
context: implicitWake.context,
|
|
123
|
+
wakeReason: implicitWake.wakeReason,
|
|
124
|
+
resumeThread: false,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
acquireIssueSessionLease(params) {
|
|
128
|
+
const now = params.now ?? isoNow();
|
|
129
|
+
const result = this.connection.prepare(`
|
|
130
|
+
UPDATE issue_sessions
|
|
131
|
+
SET lease_id = ?, worker_id = ?, leased_until = ?, updated_at = ?
|
|
132
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
133
|
+
AND (leased_until IS NULL OR leased_until <= ? OR lease_id = ?)
|
|
134
|
+
`).run(params.leaseId, params.workerId, params.leasedUntil, now, params.projectId, params.linearIssueId, now, params.leaseId);
|
|
135
|
+
return Number(result.changes ?? 0) > 0;
|
|
136
|
+
}
|
|
137
|
+
forceAcquireIssueSessionLease(params) {
|
|
138
|
+
const now = params.now ?? isoNow();
|
|
139
|
+
const result = this.connection.prepare(`
|
|
140
|
+
UPDATE issue_sessions
|
|
141
|
+
SET lease_id = ?, worker_id = ?, leased_until = ?, updated_at = ?
|
|
142
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
143
|
+
`).run(params.leaseId, params.workerId, params.leasedUntil, now, params.projectId, params.linearIssueId);
|
|
144
|
+
return Number(result.changes ?? 0) > 0;
|
|
145
|
+
}
|
|
146
|
+
renewIssueSessionLease(params) {
|
|
147
|
+
const now = params.now ?? isoNow();
|
|
148
|
+
const result = this.connection.prepare(`
|
|
149
|
+
UPDATE issue_sessions
|
|
150
|
+
SET leased_until = ?, updated_at = ?
|
|
151
|
+
WHERE project_id = ? AND linear_issue_id = ? AND lease_id = ?
|
|
152
|
+
`).run(params.leasedUntil, now, params.projectId, params.linearIssueId, params.leaseId);
|
|
153
|
+
return Number(result.changes ?? 0) > 0;
|
|
154
|
+
}
|
|
155
|
+
releaseIssueSessionLease(projectId, linearIssueId, leaseId) {
|
|
156
|
+
this.connection.prepare(`
|
|
157
|
+
UPDATE issue_sessions
|
|
158
|
+
SET lease_id = NULL, worker_id = NULL, leased_until = NULL, updated_at = ?
|
|
159
|
+
WHERE project_id = ? AND linear_issue_id = ? AND (? IS NULL OR lease_id = ?)
|
|
160
|
+
`).run(isoNow(), projectId, linearIssueId, leaseId ?? null, leaseId ?? null);
|
|
161
|
+
}
|
|
162
|
+
releaseExpiredIssueSessionLeases(now = isoNow()) {
|
|
163
|
+
this.connection.prepare(`
|
|
164
|
+
UPDATE issue_sessions
|
|
165
|
+
SET lease_id = NULL, worker_id = NULL, leased_until = NULL, updated_at = ?
|
|
166
|
+
WHERE leased_until IS NOT NULL AND leased_until <= ?
|
|
167
|
+
`).run(now, now);
|
|
168
|
+
}
|
|
169
|
+
hasActiveIssueSessionLease(projectId, linearIssueId, leaseId, now = isoNow()) {
|
|
170
|
+
const row = this.connection.prepare(`
|
|
171
|
+
SELECT 1
|
|
172
|
+
FROM issue_sessions
|
|
173
|
+
WHERE project_id = ? AND linear_issue_id = ? AND lease_id = ?
|
|
174
|
+
AND leased_until IS NOT NULL
|
|
175
|
+
AND leased_until > ?
|
|
176
|
+
LIMIT 1
|
|
177
|
+
`).get(projectId, linearIssueId, leaseId, now);
|
|
178
|
+
return row !== undefined;
|
|
179
|
+
}
|
|
180
|
+
getActiveIssueSessionLease(projectId, linearIssueId, now = isoNow()) {
|
|
181
|
+
const row = this.connection.prepare(`
|
|
182
|
+
SELECT lease_id
|
|
183
|
+
FROM issue_sessions
|
|
184
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
185
|
+
AND lease_id IS NOT NULL
|
|
186
|
+
AND leased_until IS NOT NULL
|
|
187
|
+
AND leased_until > ?
|
|
188
|
+
LIMIT 1
|
|
189
|
+
`).get(projectId, linearIssueId, now);
|
|
190
|
+
const leaseId = typeof row?.lease_id === "string" ? row.lease_id : undefined;
|
|
191
|
+
if (!leaseId)
|
|
192
|
+
return undefined;
|
|
193
|
+
return { projectId, linearIssueId, leaseId };
|
|
194
|
+
}
|
|
195
|
+
withIssueSessionLease(projectId, linearIssueId, leaseId, fn) {
|
|
196
|
+
return this.connection.transaction(() => {
|
|
197
|
+
if (!this.hasActiveIssueSessionLease(projectId, linearIssueId, leaseId)) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
return fn();
|
|
201
|
+
})();
|
|
202
|
+
}
|
|
203
|
+
upsertIssueWithLease(lease, params) {
|
|
204
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => this.issues.upsertIssue(params));
|
|
205
|
+
}
|
|
206
|
+
upsertIssueRespectingActiveLease(projectId, linearIssueId, params) {
|
|
207
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
208
|
+
if (!lease) {
|
|
209
|
+
return this.issues.upsertIssue(params);
|
|
210
|
+
}
|
|
211
|
+
return this.upsertIssueWithLease(lease, params);
|
|
212
|
+
}
|
|
213
|
+
finishRunWithLease(lease, runId, params) {
|
|
214
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
215
|
+
this.runs.finishRun(runId, params);
|
|
216
|
+
return true;
|
|
217
|
+
}) ?? false;
|
|
218
|
+
}
|
|
219
|
+
finishRunRespectingActiveLease(projectId, linearIssueId, runId, params) {
|
|
220
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
221
|
+
if (!lease) {
|
|
222
|
+
this.runs.finishRun(runId, params);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
return this.finishRunWithLease(lease, runId, params);
|
|
226
|
+
}
|
|
227
|
+
updateRunThreadWithLease(lease, runId, params) {
|
|
228
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
229
|
+
this.runs.updateRunThread(runId, params);
|
|
230
|
+
return true;
|
|
231
|
+
}) ?? false;
|
|
232
|
+
}
|
|
233
|
+
consumeIssueSessionEventsWithLease(lease, eventIds, runId) {
|
|
234
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
235
|
+
this.consumeIssueSessionEvents(lease.projectId, lease.linearIssueId, eventIds, runId);
|
|
236
|
+
return true;
|
|
237
|
+
}) ?? false;
|
|
238
|
+
}
|
|
239
|
+
clearPendingIssueSessionEventsWithLease(lease) {
|
|
240
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
241
|
+
this.clearPendingIssueSessionEvents(lease.projectId, lease.linearIssueId);
|
|
242
|
+
return true;
|
|
243
|
+
}) ?? false;
|
|
244
|
+
}
|
|
245
|
+
clearPendingIssueSessionEventsRespectingActiveLease(projectId, linearIssueId) {
|
|
246
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
247
|
+
if (!lease) {
|
|
248
|
+
this.clearPendingIssueSessionEvents(projectId, linearIssueId);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
return this.clearPendingIssueSessionEventsWithLease(lease);
|
|
252
|
+
}
|
|
253
|
+
setIssueSessionLastWakeReasonWithLease(lease, lastWakeReason) {
|
|
254
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
255
|
+
this.setIssueSessionLastWakeReason(lease.projectId, lease.linearIssueId, lastWakeReason);
|
|
256
|
+
return true;
|
|
257
|
+
}) ?? false;
|
|
258
|
+
}
|
|
259
|
+
setIssueSessionLastWakeReason(projectId, linearIssueId, lastWakeReason) {
|
|
260
|
+
this.connection.prepare(`
|
|
261
|
+
UPDATE issue_sessions
|
|
262
|
+
SET last_wake_reason = ?, updated_at = ?
|
|
263
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
264
|
+
`).run(lastWakeReason ?? null, isoNow(), projectId, linearIssueId);
|
|
265
|
+
}
|
|
266
|
+
setBranchOwnerWithLease(lease, owner) {
|
|
267
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
268
|
+
this.issues.setBranchOwner(lease.projectId, lease.linearIssueId, owner);
|
|
269
|
+
return true;
|
|
270
|
+
}) ?? false;
|
|
271
|
+
}
|
|
272
|
+
setBranchOwnerRespectingActiveLease(projectId, linearIssueId, owner) {
|
|
273
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
274
|
+
if (!lease) {
|
|
275
|
+
this.issues.setBranchOwner(projectId, linearIssueId, owner);
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
return this.setBranchOwnerWithLease(lease, owner);
|
|
279
|
+
}
|
|
280
|
+
releaseIssueSessionLeaseRespectingActiveLease(projectId, linearIssueId) {
|
|
281
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
282
|
+
this.releaseIssueSessionLease(projectId, linearIssueId, lease?.leaseId);
|
|
283
|
+
}
|
|
284
|
+
}
|