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/db.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import { isIssueSessionReadyForExecution, deriveIssueSessionState, deriveIssueSessionReactiveIntent,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { isIssueSessionReadyForExecution, deriveIssueSessionState, deriveIssueSessionReactiveIntent, } from "./issue-session.js";
|
|
2
|
+
import {} from "./issue-session-events.js";
|
|
3
|
+
import { IssueStore } from "./db/issue-store.js";
|
|
4
|
+
import { IssueSessionStore } from "./db/issue-session-store.js";
|
|
5
5
|
import { LinearInstallationStore } from "./db/linear-installation-store.js";
|
|
6
6
|
import { OperatorFeedStore } from "./db/operator-feed-store.js";
|
|
7
7
|
import { RepositoryLinkStore } from "./db/repository-link-store.js";
|
|
8
|
+
import { RunStore } from "./db/run-store.js";
|
|
9
|
+
import { WebhookEventStore } from "./db/webhook-event-store.js";
|
|
8
10
|
import { runPatchRelayMigrations } from "./db/migrations.js";
|
|
9
|
-
import { SqliteConnection
|
|
10
|
-
import {
|
|
11
|
+
import { SqliteConnection } from "./db/shared.js";
|
|
12
|
+
import { syncIssueSessionFromIssue } from "./issue-session-projector.js";
|
|
13
|
+
import { buildTrackedIssueRecord } from "./tracked-issue-projector.js";
|
|
11
14
|
function parseObjectJson(raw) {
|
|
12
15
|
if (!raw)
|
|
13
16
|
return undefined;
|
|
@@ -90,6 +93,10 @@ export class PatchRelayDatabase {
|
|
|
90
93
|
linearInstallations;
|
|
91
94
|
operatorFeed;
|
|
92
95
|
repositories;
|
|
96
|
+
webhookEvents;
|
|
97
|
+
issues;
|
|
98
|
+
issueSessions;
|
|
99
|
+
runs;
|
|
93
100
|
constructor(databasePath, wal) {
|
|
94
101
|
this.connection = new SqliteConnection(databasePath);
|
|
95
102
|
this.connection.pragma("foreign_keys = ON");
|
|
@@ -99,6 +106,17 @@ export class PatchRelayDatabase {
|
|
|
99
106
|
this.linearInstallations = new LinearInstallationStore(this.connection);
|
|
100
107
|
this.operatorFeed = new OperatorFeedStore(this.connection);
|
|
101
108
|
this.repositories = new RepositoryLinkStore(this.connection);
|
|
109
|
+
this.webhookEvents = new WebhookEventStore(this.connection);
|
|
110
|
+
this.issues = new IssueStore(this.connection, (issue) => syncIssueSessionFromIssue({ connection: this.connection, issues: this.issues, issueSessions: this.issueSessions, runs: this.runs, issue }));
|
|
111
|
+
this.runs = new RunStore(this.connection, mapRunRow, this.issues, (issue, options) => syncIssueSessionFromIssue({
|
|
112
|
+
connection: this.connection,
|
|
113
|
+
issues: this.issues,
|
|
114
|
+
issueSessions: this.issueSessions,
|
|
115
|
+
runs: this.runs,
|
|
116
|
+
issue,
|
|
117
|
+
...(options ? { options } : {}),
|
|
118
|
+
}));
|
|
119
|
+
this.issueSessions = new IssueSessionStore(this.connection, mapIssueSessionRow, mapIssueSessionEventRow, this.issues, this.runs, deriveImplicitReactiveWake);
|
|
102
120
|
}
|
|
103
121
|
runMigrations() {
|
|
104
122
|
runPatchRelayMigrations(this.connection);
|
|
@@ -106,721 +124,44 @@ export class PatchRelayDatabase {
|
|
|
106
124
|
transaction(fn) {
|
|
107
125
|
return this.connection.transaction(fn)();
|
|
108
126
|
}
|
|
109
|
-
// ─── Webhook Events ───────────────────────────────────────────────
|
|
110
|
-
insertWebhookEvent(webhookId, receivedAt) {
|
|
111
|
-
const existing = this.connection
|
|
112
|
-
.prepare("SELECT id FROM webhook_events WHERE webhook_id = ?")
|
|
113
|
-
.get(webhookId);
|
|
114
|
-
if (existing) {
|
|
115
|
-
return { id: existing.id, duplicate: true };
|
|
116
|
-
}
|
|
117
|
-
const result = this.connection
|
|
118
|
-
.prepare("INSERT INTO webhook_events (webhook_id, received_at, processing_status) VALUES (?, ?, 'processed')")
|
|
119
|
-
.run(webhookId, receivedAt);
|
|
120
|
-
return { id: Number(result.lastInsertRowid), duplicate: false };
|
|
121
|
-
}
|
|
122
|
-
insertFullWebhookEvent(params) {
|
|
123
|
-
const existing = this.connection
|
|
124
|
-
.prepare("SELECT id FROM webhook_events WHERE webhook_id = ?")
|
|
125
|
-
.get(params.webhookId);
|
|
126
|
-
if (existing) {
|
|
127
|
-
return { id: existing.id, dedupeStatus: "duplicate" };
|
|
128
|
-
}
|
|
129
|
-
const result = this.connection
|
|
130
|
-
.prepare("INSERT INTO webhook_events (webhook_id, received_at, payload_json) VALUES (?, ?, ?)")
|
|
131
|
-
.run(params.webhookId, params.receivedAt, params.payloadJson);
|
|
132
|
-
return { id: Number(result.lastInsertRowid), dedupeStatus: "accepted" };
|
|
133
|
-
}
|
|
134
|
-
getWebhookPayload(id) {
|
|
135
|
-
const row = this.connection.prepare("SELECT webhook_id, payload_json FROM webhook_events WHERE id = ?").get(id);
|
|
136
|
-
if (!row || !row.payload_json)
|
|
137
|
-
return undefined;
|
|
138
|
-
return { webhookId: String(row.webhook_id), payloadJson: String(row.payload_json) };
|
|
139
|
-
}
|
|
140
|
-
isWebhookDuplicate(webhookId) {
|
|
141
|
-
return this.connection.prepare("SELECT 1 FROM webhook_events WHERE webhook_id = ?").get(webhookId) !== undefined;
|
|
142
|
-
}
|
|
143
|
-
markWebhookProcessed(id, status) {
|
|
144
|
-
this.connection.prepare("UPDATE webhook_events SET processing_status = ? WHERE id = ?").run(status, id);
|
|
145
|
-
}
|
|
146
|
-
assignWebhookProject(id, projectId) {
|
|
147
|
-
this.connection.prepare("UPDATE webhook_events SET project_id = ? WHERE id = ?").run(projectId, id);
|
|
148
|
-
}
|
|
149
|
-
// ─── Issues ───────────────────────────────────────────────────────
|
|
150
127
|
upsertIssue(params) {
|
|
151
|
-
|
|
152
|
-
const existing = this.getIssue(params.projectId, params.linearIssueId);
|
|
153
|
-
if (existing) {
|
|
154
|
-
// Build dynamic SET clauses for nullable fields
|
|
155
|
-
const sets = ["updated_at = @now"];
|
|
156
|
-
const values = {
|
|
157
|
-
now,
|
|
158
|
-
projectId: params.projectId,
|
|
159
|
-
linearIssueId: params.linearIssueId,
|
|
160
|
-
};
|
|
161
|
-
if (params.issueKey !== undefined) {
|
|
162
|
-
sets.push("issue_key = COALESCE(@issueKey, issue_key)");
|
|
163
|
-
values.issueKey = params.issueKey;
|
|
164
|
-
}
|
|
165
|
-
if (params.title !== undefined) {
|
|
166
|
-
sets.push("title = COALESCE(@title, title)");
|
|
167
|
-
values.title = params.title;
|
|
168
|
-
}
|
|
169
|
-
if (params.description !== undefined) {
|
|
170
|
-
sets.push("description = COALESCE(@description, description)");
|
|
171
|
-
values.description = params.description;
|
|
172
|
-
}
|
|
173
|
-
if (params.url !== undefined) {
|
|
174
|
-
sets.push("url = COALESCE(@url, url)");
|
|
175
|
-
values.url = params.url;
|
|
176
|
-
}
|
|
177
|
-
if (params.priority !== undefined) {
|
|
178
|
-
sets.push("priority = @priority");
|
|
179
|
-
values.priority = params.priority;
|
|
180
|
-
}
|
|
181
|
-
if (params.estimate !== undefined) {
|
|
182
|
-
sets.push("estimate = @estimate");
|
|
183
|
-
values.estimate = params.estimate;
|
|
184
|
-
}
|
|
185
|
-
if (params.currentLinearState !== undefined) {
|
|
186
|
-
sets.push("current_linear_state = COALESCE(@currentLinearState, current_linear_state)");
|
|
187
|
-
values.currentLinearState = params.currentLinearState;
|
|
188
|
-
}
|
|
189
|
-
if (params.currentLinearStateType !== undefined) {
|
|
190
|
-
sets.push("current_linear_state_type = COALESCE(@currentLinearStateType, current_linear_state_type)");
|
|
191
|
-
values.currentLinearStateType = params.currentLinearStateType;
|
|
192
|
-
}
|
|
193
|
-
if (params.factoryState !== undefined) {
|
|
194
|
-
sets.push("factory_state = @factoryState");
|
|
195
|
-
values.factoryState = params.factoryState;
|
|
196
|
-
}
|
|
197
|
-
if (params.pendingRunType !== undefined) {
|
|
198
|
-
sets.push("pending_run_type = @pendingRunType");
|
|
199
|
-
values.pendingRunType = params.pendingRunType;
|
|
200
|
-
}
|
|
201
|
-
if (params.pendingRunContextJson !== undefined) {
|
|
202
|
-
sets.push("pending_run_context_json = @pendingRunContextJson");
|
|
203
|
-
values.pendingRunContextJson = params.pendingRunContextJson;
|
|
204
|
-
}
|
|
205
|
-
if (params.branchName !== undefined) {
|
|
206
|
-
sets.push("branch_name = COALESCE(@branchName, branch_name)");
|
|
207
|
-
values.branchName = params.branchName;
|
|
208
|
-
}
|
|
209
|
-
if (params.worktreePath !== undefined) {
|
|
210
|
-
sets.push("worktree_path = COALESCE(@worktreePath, worktree_path)");
|
|
211
|
-
values.worktreePath = params.worktreePath;
|
|
212
|
-
}
|
|
213
|
-
if (params.threadId !== undefined) {
|
|
214
|
-
sets.push("thread_id = @threadId");
|
|
215
|
-
values.threadId = params.threadId;
|
|
216
|
-
}
|
|
217
|
-
if (params.activeRunId !== undefined) {
|
|
218
|
-
sets.push("active_run_id = @activeRunId");
|
|
219
|
-
values.activeRunId = params.activeRunId;
|
|
220
|
-
}
|
|
221
|
-
if (params.statusCommentId !== undefined) {
|
|
222
|
-
sets.push("status_comment_id = @statusCommentId");
|
|
223
|
-
values.statusCommentId = params.statusCommentId;
|
|
224
|
-
}
|
|
225
|
-
if (params.agentSessionId !== undefined) {
|
|
226
|
-
sets.push("agent_session_id = @agentSessionId");
|
|
227
|
-
values.agentSessionId = params.agentSessionId;
|
|
228
|
-
}
|
|
229
|
-
if (params.prNumber !== undefined) {
|
|
230
|
-
sets.push("pr_number = @prNumber");
|
|
231
|
-
values.prNumber = params.prNumber;
|
|
232
|
-
}
|
|
233
|
-
if (params.prUrl !== undefined) {
|
|
234
|
-
sets.push("pr_url = @prUrl");
|
|
235
|
-
values.prUrl = params.prUrl;
|
|
236
|
-
}
|
|
237
|
-
if (params.prState !== undefined) {
|
|
238
|
-
sets.push("pr_state = @prState");
|
|
239
|
-
values.prState = params.prState;
|
|
240
|
-
}
|
|
241
|
-
if (params.prHeadSha !== undefined) {
|
|
242
|
-
sets.push("pr_head_sha = @prHeadSha");
|
|
243
|
-
values.prHeadSha = params.prHeadSha;
|
|
244
|
-
}
|
|
245
|
-
if (params.prAuthorLogin !== undefined) {
|
|
246
|
-
sets.push("pr_author_login = @prAuthorLogin");
|
|
247
|
-
values.prAuthorLogin = params.prAuthorLogin;
|
|
248
|
-
}
|
|
249
|
-
if (params.prReviewState !== undefined) {
|
|
250
|
-
sets.push("pr_review_state = @prReviewState");
|
|
251
|
-
values.prReviewState = params.prReviewState;
|
|
252
|
-
}
|
|
253
|
-
if (params.prCheckStatus !== undefined) {
|
|
254
|
-
sets.push("pr_check_status = @prCheckStatus");
|
|
255
|
-
values.prCheckStatus = params.prCheckStatus;
|
|
256
|
-
}
|
|
257
|
-
if (params.lastBlockingReviewHeadSha !== undefined) {
|
|
258
|
-
sets.push("last_blocking_review_head_sha = @lastBlockingReviewHeadSha");
|
|
259
|
-
values.lastBlockingReviewHeadSha = params.lastBlockingReviewHeadSha;
|
|
260
|
-
}
|
|
261
|
-
if (params.lastGitHubFailureSource !== undefined) {
|
|
262
|
-
sets.push("last_github_failure_source = @lastGitHubFailureSource");
|
|
263
|
-
values.lastGitHubFailureSource = params.lastGitHubFailureSource;
|
|
264
|
-
}
|
|
265
|
-
if (params.lastGitHubFailureHeadSha !== undefined) {
|
|
266
|
-
sets.push("last_github_failure_head_sha = @lastGitHubFailureHeadSha");
|
|
267
|
-
values.lastGitHubFailureHeadSha = params.lastGitHubFailureHeadSha;
|
|
268
|
-
}
|
|
269
|
-
if (params.lastGitHubFailureSignature !== undefined) {
|
|
270
|
-
sets.push("last_github_failure_signature = @lastGitHubFailureSignature");
|
|
271
|
-
values.lastGitHubFailureSignature = params.lastGitHubFailureSignature;
|
|
272
|
-
}
|
|
273
|
-
if (params.lastGitHubFailureCheckName !== undefined) {
|
|
274
|
-
sets.push("last_github_failure_check_name = @lastGitHubFailureCheckName");
|
|
275
|
-
values.lastGitHubFailureCheckName = params.lastGitHubFailureCheckName;
|
|
276
|
-
}
|
|
277
|
-
if (params.lastGitHubFailureCheckUrl !== undefined) {
|
|
278
|
-
sets.push("last_github_failure_check_url = @lastGitHubFailureCheckUrl");
|
|
279
|
-
values.lastGitHubFailureCheckUrl = params.lastGitHubFailureCheckUrl;
|
|
280
|
-
}
|
|
281
|
-
if (params.lastGitHubFailureContextJson !== undefined) {
|
|
282
|
-
sets.push("last_github_failure_context_json = @lastGitHubFailureContextJson");
|
|
283
|
-
values.lastGitHubFailureContextJson = params.lastGitHubFailureContextJson;
|
|
284
|
-
}
|
|
285
|
-
if (params.lastGitHubFailureAt !== undefined) {
|
|
286
|
-
sets.push("last_github_failure_at = @lastGitHubFailureAt");
|
|
287
|
-
values.lastGitHubFailureAt = params.lastGitHubFailureAt;
|
|
288
|
-
}
|
|
289
|
-
if (params.lastGitHubCiSnapshotHeadSha !== undefined) {
|
|
290
|
-
sets.push("last_github_ci_snapshot_head_sha = @lastGitHubCiSnapshotHeadSha");
|
|
291
|
-
values.lastGitHubCiSnapshotHeadSha = params.lastGitHubCiSnapshotHeadSha;
|
|
292
|
-
}
|
|
293
|
-
if (params.lastGitHubCiSnapshotGateCheckName !== undefined) {
|
|
294
|
-
sets.push("last_github_ci_snapshot_gate_check_name = @lastGitHubCiSnapshotGateCheckName");
|
|
295
|
-
values.lastGitHubCiSnapshotGateCheckName = params.lastGitHubCiSnapshotGateCheckName;
|
|
296
|
-
}
|
|
297
|
-
if (params.lastGitHubCiSnapshotGateCheckStatus !== undefined) {
|
|
298
|
-
sets.push("last_github_ci_snapshot_gate_check_status = @lastGitHubCiSnapshotGateCheckStatus");
|
|
299
|
-
values.lastGitHubCiSnapshotGateCheckStatus = params.lastGitHubCiSnapshotGateCheckStatus;
|
|
300
|
-
}
|
|
301
|
-
if (params.lastGitHubCiSnapshotJson !== undefined) {
|
|
302
|
-
sets.push("last_github_ci_snapshot_json = @lastGitHubCiSnapshotJson");
|
|
303
|
-
values.lastGitHubCiSnapshotJson = params.lastGitHubCiSnapshotJson;
|
|
304
|
-
}
|
|
305
|
-
if (params.lastGitHubCiSnapshotSettledAt !== undefined) {
|
|
306
|
-
sets.push("last_github_ci_snapshot_settled_at = @lastGitHubCiSnapshotSettledAt");
|
|
307
|
-
values.lastGitHubCiSnapshotSettledAt = params.lastGitHubCiSnapshotSettledAt;
|
|
308
|
-
}
|
|
309
|
-
if (params.lastQueueSignalAt !== undefined) {
|
|
310
|
-
sets.push("last_queue_signal_at = @lastQueueSignalAt");
|
|
311
|
-
values.lastQueueSignalAt = params.lastQueueSignalAt;
|
|
312
|
-
}
|
|
313
|
-
if (params.lastQueueIncidentJson !== undefined) {
|
|
314
|
-
sets.push("last_queue_incident_json = @lastQueueIncidentJson");
|
|
315
|
-
values.lastQueueIncidentJson = params.lastQueueIncidentJson;
|
|
316
|
-
}
|
|
317
|
-
if (params.lastAttemptedFailureHeadSha !== undefined) {
|
|
318
|
-
sets.push("last_attempted_failure_head_sha = @lastAttemptedFailureHeadSha");
|
|
319
|
-
values.lastAttemptedFailureHeadSha = params.lastAttemptedFailureHeadSha;
|
|
320
|
-
}
|
|
321
|
-
if (params.lastAttemptedFailureSignature !== undefined) {
|
|
322
|
-
sets.push("last_attempted_failure_signature = @lastAttemptedFailureSignature");
|
|
323
|
-
values.lastAttemptedFailureSignature = params.lastAttemptedFailureSignature;
|
|
324
|
-
}
|
|
325
|
-
if (params.ciRepairAttempts !== undefined) {
|
|
326
|
-
sets.push("ci_repair_attempts = @ciRepairAttempts");
|
|
327
|
-
values.ciRepairAttempts = params.ciRepairAttempts;
|
|
328
|
-
}
|
|
329
|
-
if (params.queueRepairAttempts !== undefined) {
|
|
330
|
-
sets.push("queue_repair_attempts = @queueRepairAttempts");
|
|
331
|
-
values.queueRepairAttempts = params.queueRepairAttempts;
|
|
332
|
-
}
|
|
333
|
-
if (params.reviewFixAttempts !== undefined) {
|
|
334
|
-
sets.push("review_fix_attempts = @reviewFixAttempts");
|
|
335
|
-
values.reviewFixAttempts = params.reviewFixAttempts;
|
|
336
|
-
}
|
|
337
|
-
if (params.zombieRecoveryAttempts !== undefined) {
|
|
338
|
-
sets.push("zombie_recovery_attempts = @zombieRecoveryAttempts");
|
|
339
|
-
values.zombieRecoveryAttempts = params.zombieRecoveryAttempts;
|
|
340
|
-
}
|
|
341
|
-
if (params.lastZombieRecoveryAt !== undefined) {
|
|
342
|
-
sets.push("last_zombie_recovery_at = @lastZombieRecoveryAt");
|
|
343
|
-
values.lastZombieRecoveryAt = params.lastZombieRecoveryAt;
|
|
344
|
-
}
|
|
345
|
-
this.connection.prepare(`UPDATE issues SET ${sets.join(", ")} WHERE project_id = @projectId AND linear_issue_id = @linearIssueId`).run(values);
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
this.connection.prepare(`
|
|
349
|
-
INSERT INTO issues (
|
|
350
|
-
project_id, linear_issue_id, issue_key, title, description, url,
|
|
351
|
-
priority, estimate,
|
|
352
|
-
current_linear_state, current_linear_state_type, factory_state, pending_run_type, pending_run_context_json,
|
|
353
|
-
branch_name, worktree_path, thread_id, active_run_id, status_comment_id,
|
|
354
|
-
agent_session_id,
|
|
355
|
-
pr_number, pr_url, pr_state, pr_head_sha, pr_author_login, pr_review_state, pr_check_status, last_blocking_review_head_sha,
|
|
356
|
-
last_github_failure_source, last_github_failure_head_sha, last_github_failure_signature, last_github_failure_check_name, last_github_failure_check_url, last_github_failure_context_json, last_github_failure_at,
|
|
357
|
-
last_github_ci_snapshot_head_sha, last_github_ci_snapshot_gate_check_name, last_github_ci_snapshot_gate_check_status, last_github_ci_snapshot_json, last_github_ci_snapshot_settled_at,
|
|
358
|
-
last_queue_signal_at, last_queue_incident_json,
|
|
359
|
-
last_attempted_failure_head_sha, last_attempted_failure_signature,
|
|
360
|
-
ci_repair_attempts, queue_repair_attempts, review_fix_attempts, zombie_recovery_attempts, last_zombie_recovery_at,
|
|
361
|
-
updated_at
|
|
362
|
-
) VALUES (
|
|
363
|
-
@projectId, @linearIssueId, @issueKey, @title, @description, @url,
|
|
364
|
-
@priority, @estimate,
|
|
365
|
-
@currentLinearState, @currentLinearStateType, @factoryState, @pendingRunType, @pendingRunContextJson,
|
|
366
|
-
@branchName, @worktreePath, @threadId, @activeRunId, @statusCommentId,
|
|
367
|
-
@agentSessionId,
|
|
368
|
-
@prNumber, @prUrl, @prState, @prHeadSha, @prAuthorLogin, @prReviewState, @prCheckStatus, @lastBlockingReviewHeadSha,
|
|
369
|
-
@lastGitHubFailureSource, @lastGitHubFailureHeadSha, @lastGitHubFailureSignature, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureContextJson, @lastGitHubFailureAt,
|
|
370
|
-
@lastGitHubCiSnapshotHeadSha, @lastGitHubCiSnapshotGateCheckName, @lastGitHubCiSnapshotGateCheckStatus, @lastGitHubCiSnapshotJson, @lastGitHubCiSnapshotSettledAt,
|
|
371
|
-
@lastQueueSignalAt, @lastQueueIncidentJson,
|
|
372
|
-
@lastAttemptedFailureHeadSha, @lastAttemptedFailureSignature,
|
|
373
|
-
@ciRepairAttempts, @queueRepairAttempts, @reviewFixAttempts, @zombieRecoveryAttempts, @lastZombieRecoveryAt,
|
|
374
|
-
@now
|
|
375
|
-
)
|
|
376
|
-
`).run({
|
|
377
|
-
projectId: params.projectId,
|
|
378
|
-
linearIssueId: params.linearIssueId,
|
|
379
|
-
issueKey: params.issueKey ?? null,
|
|
380
|
-
title: params.title ?? null,
|
|
381
|
-
description: params.description ?? null,
|
|
382
|
-
url: params.url ?? null,
|
|
383
|
-
priority: params.priority ?? null,
|
|
384
|
-
estimate: params.estimate ?? null,
|
|
385
|
-
currentLinearState: params.currentLinearState ?? null,
|
|
386
|
-
currentLinearStateType: params.currentLinearStateType ?? null,
|
|
387
|
-
factoryState: params.factoryState ?? "delegated",
|
|
388
|
-
pendingRunType: params.pendingRunType ?? null,
|
|
389
|
-
pendingRunContextJson: params.pendingRunContextJson ?? null,
|
|
390
|
-
branchName: params.branchName ?? null,
|
|
391
|
-
worktreePath: params.worktreePath ?? null,
|
|
392
|
-
threadId: params.threadId ?? null,
|
|
393
|
-
activeRunId: params.activeRunId ?? null,
|
|
394
|
-
statusCommentId: params.statusCommentId ?? null,
|
|
395
|
-
agentSessionId: params.agentSessionId ?? null,
|
|
396
|
-
prNumber: params.prNumber ?? null,
|
|
397
|
-
prUrl: params.prUrl ?? null,
|
|
398
|
-
prState: params.prState ?? null,
|
|
399
|
-
prHeadSha: params.prHeadSha ?? null,
|
|
400
|
-
prAuthorLogin: params.prAuthorLogin ?? null,
|
|
401
|
-
prReviewState: params.prReviewState ?? null,
|
|
402
|
-
prCheckStatus: params.prCheckStatus ?? null,
|
|
403
|
-
lastBlockingReviewHeadSha: params.lastBlockingReviewHeadSha ?? null,
|
|
404
|
-
lastGitHubFailureSource: params.lastGitHubFailureSource ?? null,
|
|
405
|
-
lastGitHubFailureHeadSha: params.lastGitHubFailureHeadSha ?? null,
|
|
406
|
-
lastGitHubFailureSignature: params.lastGitHubFailureSignature ?? null,
|
|
407
|
-
lastGitHubFailureCheckName: params.lastGitHubFailureCheckName ?? null,
|
|
408
|
-
lastGitHubFailureCheckUrl: params.lastGitHubFailureCheckUrl ?? null,
|
|
409
|
-
lastGitHubFailureContextJson: params.lastGitHubFailureContextJson ?? null,
|
|
410
|
-
lastGitHubFailureAt: params.lastGitHubFailureAt ?? null,
|
|
411
|
-
lastGitHubCiSnapshotHeadSha: params.lastGitHubCiSnapshotHeadSha ?? null,
|
|
412
|
-
lastGitHubCiSnapshotGateCheckName: params.lastGitHubCiSnapshotGateCheckName ?? null,
|
|
413
|
-
lastGitHubCiSnapshotGateCheckStatus: params.lastGitHubCiSnapshotGateCheckStatus ?? null,
|
|
414
|
-
lastGitHubCiSnapshotJson: params.lastGitHubCiSnapshotJson ?? null,
|
|
415
|
-
lastGitHubCiSnapshotSettledAt: params.lastGitHubCiSnapshotSettledAt ?? null,
|
|
416
|
-
lastQueueSignalAt: params.lastQueueSignalAt ?? null,
|
|
417
|
-
lastQueueIncidentJson: params.lastQueueIncidentJson ?? null,
|
|
418
|
-
lastAttemptedFailureHeadSha: params.lastAttemptedFailureHeadSha ?? null,
|
|
419
|
-
lastAttemptedFailureSignature: params.lastAttemptedFailureSignature ?? null,
|
|
420
|
-
ciRepairAttempts: params.ciRepairAttempts ?? 0,
|
|
421
|
-
queueRepairAttempts: params.queueRepairAttempts ?? 0,
|
|
422
|
-
reviewFixAttempts: params.reviewFixAttempts ?? 0,
|
|
423
|
-
zombieRecoveryAttempts: params.zombieRecoveryAttempts ?? 0,
|
|
424
|
-
lastZombieRecoveryAt: params.lastZombieRecoveryAt ?? null,
|
|
425
|
-
now,
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
const updated = this.getIssue(params.projectId, params.linearIssueId);
|
|
429
|
-
this.syncIssueSessionFromIssue(updated);
|
|
430
|
-
return updated;
|
|
128
|
+
return this.issues.upsertIssue(params);
|
|
431
129
|
}
|
|
432
130
|
getIssue(projectId, linearIssueId) {
|
|
433
|
-
|
|
434
|
-
.prepare("SELECT * FROM issues WHERE project_id = ? AND linear_issue_id = ?")
|
|
435
|
-
.get(projectId, linearIssueId);
|
|
436
|
-
return row ? mapIssueRow(row) : undefined;
|
|
131
|
+
return this.issues.getIssue(projectId, linearIssueId);
|
|
437
132
|
}
|
|
438
133
|
getIssueById(id) {
|
|
439
|
-
|
|
440
|
-
return row ? mapIssueRow(row) : undefined;
|
|
134
|
+
return this.issues.getIssueById(id);
|
|
441
135
|
}
|
|
442
136
|
getIssueByKey(issueKey) {
|
|
443
|
-
|
|
444
|
-
return row ? mapIssueRow(row) : undefined;
|
|
137
|
+
return this.issues.getIssueByKey(issueKey);
|
|
445
138
|
}
|
|
446
139
|
getIssueByBranch(branchName) {
|
|
447
|
-
|
|
448
|
-
return row ? mapIssueRow(row) : undefined;
|
|
140
|
+
return this.issues.getIssueByBranch(branchName);
|
|
449
141
|
}
|
|
450
142
|
getIssueByPrNumber(prNumber) {
|
|
451
|
-
|
|
452
|
-
return row ? mapIssueRow(row) : undefined;
|
|
453
|
-
}
|
|
454
|
-
getIssueSession(projectId, linearIssueId) {
|
|
455
|
-
const row = this.connection
|
|
456
|
-
.prepare("SELECT * FROM issue_sessions WHERE project_id = ? AND linear_issue_id = ?")
|
|
457
|
-
.get(projectId, linearIssueId);
|
|
458
|
-
return row ? mapIssueSessionRow(row) : undefined;
|
|
459
|
-
}
|
|
460
|
-
getIssueSessionByKey(issueKey) {
|
|
461
|
-
const row = this.connection.prepare("SELECT * FROM issue_sessions WHERE issue_key = ?").get(issueKey);
|
|
462
|
-
return row ? mapIssueSessionRow(row) : undefined;
|
|
463
|
-
}
|
|
464
|
-
appendIssueSessionEvent(params) {
|
|
465
|
-
if (params.dedupeKey) {
|
|
466
|
-
const existing = this.connection.prepare(`
|
|
467
|
-
SELECT * FROM issue_session_events
|
|
468
|
-
WHERE project_id = ? AND linear_issue_id = ? AND dedupe_key = ? AND processed_at IS NULL
|
|
469
|
-
ORDER BY id DESC LIMIT 1
|
|
470
|
-
`).get(params.projectId, params.linearIssueId, params.dedupeKey);
|
|
471
|
-
if (existing)
|
|
472
|
-
return mapIssueSessionEventRow(existing);
|
|
473
|
-
}
|
|
474
|
-
const now = isoNow();
|
|
475
|
-
const result = this.connection.prepare(`
|
|
476
|
-
INSERT INTO issue_session_events (
|
|
477
|
-
project_id, linear_issue_id, event_type, event_json, dedupe_key, created_at
|
|
478
|
-
) VALUES (?, ?, ?, ?, ?, ?)
|
|
479
|
-
`).run(params.projectId, params.linearIssueId, params.eventType, params.eventJson ?? null, params.dedupeKey ?? null, now);
|
|
480
|
-
return this.getIssueSessionEvent(Number(result.lastInsertRowid));
|
|
481
|
-
}
|
|
482
|
-
appendIssueSessionEventWithLease(lease, params) {
|
|
483
|
-
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => this.appendIssueSessionEvent(params));
|
|
484
|
-
}
|
|
485
|
-
appendIssueSessionEventRespectingActiveLease(projectId, linearIssueId, params) {
|
|
486
|
-
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
487
|
-
if (!lease) {
|
|
488
|
-
return this.appendIssueSessionEvent(params);
|
|
489
|
-
}
|
|
490
|
-
return this.appendIssueSessionEventWithLease(lease, params);
|
|
491
|
-
}
|
|
492
|
-
getIssueSessionEvent(id) {
|
|
493
|
-
const row = this.connection.prepare("SELECT * FROM issue_session_events WHERE id = ?").get(id);
|
|
494
|
-
return row ? mapIssueSessionEventRow(row) : undefined;
|
|
495
|
-
}
|
|
496
|
-
listIssueSessionEvents(projectId, linearIssueId, options) {
|
|
497
|
-
const conditions = ["project_id = ?", "linear_issue_id = ?"];
|
|
498
|
-
const values = [projectId, linearIssueId];
|
|
499
|
-
if (options?.pendingOnly) {
|
|
500
|
-
conditions.push("processed_at IS NULL");
|
|
501
|
-
}
|
|
502
|
-
let query = `SELECT * FROM issue_session_events WHERE ${conditions.join(" AND ")} ORDER BY id`;
|
|
503
|
-
if (options?.limit !== undefined) {
|
|
504
|
-
query += " LIMIT ?";
|
|
505
|
-
values.push(options.limit);
|
|
506
|
-
}
|
|
507
|
-
const rows = this.connection.prepare(query).all(...values);
|
|
508
|
-
return rows.map(mapIssueSessionEventRow);
|
|
509
|
-
}
|
|
510
|
-
consumeIssueSessionEvents(projectId, linearIssueId, eventIds, runId) {
|
|
511
|
-
if (eventIds.length === 0)
|
|
512
|
-
return;
|
|
513
|
-
const now = isoNow();
|
|
514
|
-
const placeholders = eventIds.map(() => "?").join(", ");
|
|
515
|
-
this.connection.prepare(`
|
|
516
|
-
UPDATE issue_session_events
|
|
517
|
-
SET processed_at = ?, consumed_by_run_id = ?
|
|
518
|
-
WHERE project_id = ? AND linear_issue_id = ? AND id IN (${placeholders}) AND processed_at IS NULL
|
|
519
|
-
`).run(now, runId, projectId, linearIssueId, ...eventIds);
|
|
520
|
-
}
|
|
521
|
-
clearPendingIssueSessionEvents(projectId, linearIssueId) {
|
|
522
|
-
this.connection.prepare(`
|
|
523
|
-
UPDATE issue_session_events
|
|
524
|
-
SET processed_at = ?, consumed_by_run_id = NULL
|
|
525
|
-
WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
|
|
526
|
-
`).run(isoNow(), projectId, linearIssueId);
|
|
527
|
-
}
|
|
528
|
-
hasPendingIssueSessionEvents(projectId, linearIssueId) {
|
|
529
|
-
const row = this.connection.prepare(`
|
|
530
|
-
SELECT 1
|
|
531
|
-
FROM issue_session_events
|
|
532
|
-
WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
|
|
533
|
-
LIMIT 1
|
|
534
|
-
`).get(projectId, linearIssueId);
|
|
535
|
-
return row !== undefined;
|
|
536
|
-
}
|
|
537
|
-
peekIssueSessionWake(projectId, linearIssueId) {
|
|
538
|
-
const issue = this.getIssue(projectId, linearIssueId);
|
|
539
|
-
if (!issue)
|
|
540
|
-
return undefined;
|
|
541
|
-
const events = this.listIssueSessionEvents(projectId, linearIssueId, { pendingOnly: true });
|
|
542
|
-
const plan = deriveSessionWakePlan(issue, events);
|
|
543
|
-
if (plan?.runType) {
|
|
544
|
-
return {
|
|
545
|
-
eventIds: events.map((event) => event.id),
|
|
546
|
-
runType: plan.runType,
|
|
547
|
-
context: plan.context,
|
|
548
|
-
...(plan.wakeReason ? { wakeReason: plan.wakeReason } : {}),
|
|
549
|
-
resumeThread: plan.resumeThread,
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
const implicitWake = deriveImplicitReactiveWake(issue);
|
|
553
|
-
if (!implicitWake)
|
|
554
|
-
return undefined;
|
|
555
|
-
return {
|
|
556
|
-
eventIds: [],
|
|
557
|
-
runType: implicitWake.runType,
|
|
558
|
-
context: implicitWake.context,
|
|
559
|
-
wakeReason: implicitWake.wakeReason,
|
|
560
|
-
resumeThread: false,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
acquireIssueSessionLease(params) {
|
|
564
|
-
const now = params.now ?? isoNow();
|
|
565
|
-
const result = this.connection.prepare(`
|
|
566
|
-
UPDATE issue_sessions
|
|
567
|
-
SET lease_id = ?, worker_id = ?, leased_until = ?, updated_at = ?
|
|
568
|
-
WHERE project_id = ? AND linear_issue_id = ?
|
|
569
|
-
AND (leased_until IS NULL OR leased_until <= ? OR lease_id = ?)
|
|
570
|
-
`).run(params.leaseId, params.workerId, params.leasedUntil, now, params.projectId, params.linearIssueId, now, params.leaseId);
|
|
571
|
-
return Number(result.changes ?? 0) > 0;
|
|
572
|
-
}
|
|
573
|
-
forceAcquireIssueSessionLease(params) {
|
|
574
|
-
const now = params.now ?? isoNow();
|
|
575
|
-
const result = this.connection.prepare(`
|
|
576
|
-
UPDATE issue_sessions
|
|
577
|
-
SET lease_id = ?, worker_id = ?, leased_until = ?, updated_at = ?
|
|
578
|
-
WHERE project_id = ? AND linear_issue_id = ?
|
|
579
|
-
`).run(params.leaseId, params.workerId, params.leasedUntil, now, params.projectId, params.linearIssueId);
|
|
580
|
-
return Number(result.changes ?? 0) > 0;
|
|
581
|
-
}
|
|
582
|
-
renewIssueSessionLease(params) {
|
|
583
|
-
const now = params.now ?? isoNow();
|
|
584
|
-
const result = this.connection.prepare(`
|
|
585
|
-
UPDATE issue_sessions
|
|
586
|
-
SET leased_until = ?, updated_at = ?
|
|
587
|
-
WHERE project_id = ? AND linear_issue_id = ? AND lease_id = ?
|
|
588
|
-
`).run(params.leasedUntil, now, params.projectId, params.linearIssueId, params.leaseId);
|
|
589
|
-
return Number(result.changes ?? 0) > 0;
|
|
590
|
-
}
|
|
591
|
-
releaseIssueSessionLease(projectId, linearIssueId, leaseId) {
|
|
592
|
-
this.connection.prepare(`
|
|
593
|
-
UPDATE issue_sessions
|
|
594
|
-
SET lease_id = NULL, worker_id = NULL, leased_until = NULL, updated_at = ?
|
|
595
|
-
WHERE project_id = ? AND linear_issue_id = ? AND (? IS NULL OR lease_id = ?)
|
|
596
|
-
`).run(isoNow(), projectId, linearIssueId, leaseId ?? null, leaseId ?? null);
|
|
597
|
-
}
|
|
598
|
-
releaseExpiredIssueSessionLeases(now = isoNow()) {
|
|
599
|
-
this.connection.prepare(`
|
|
600
|
-
UPDATE issue_sessions
|
|
601
|
-
SET lease_id = NULL, worker_id = NULL, leased_until = NULL, updated_at = ?
|
|
602
|
-
WHERE leased_until IS NOT NULL AND leased_until <= ?
|
|
603
|
-
`).run(now, now);
|
|
604
|
-
}
|
|
605
|
-
hasActiveIssueSessionLease(projectId, linearIssueId, leaseId, now = isoNow()) {
|
|
606
|
-
const row = this.connection.prepare(`
|
|
607
|
-
SELECT 1
|
|
608
|
-
FROM issue_sessions
|
|
609
|
-
WHERE project_id = ? AND linear_issue_id = ? AND lease_id = ?
|
|
610
|
-
AND leased_until IS NOT NULL
|
|
611
|
-
AND leased_until > ?
|
|
612
|
-
LIMIT 1
|
|
613
|
-
`).get(projectId, linearIssueId, leaseId, now);
|
|
614
|
-
return row !== undefined;
|
|
615
|
-
}
|
|
616
|
-
getActiveIssueSessionLease(projectId, linearIssueId, now = isoNow()) {
|
|
617
|
-
const row = this.connection.prepare(`
|
|
618
|
-
SELECT lease_id
|
|
619
|
-
FROM issue_sessions
|
|
620
|
-
WHERE project_id = ? AND linear_issue_id = ?
|
|
621
|
-
AND lease_id IS NOT NULL
|
|
622
|
-
AND leased_until IS NOT NULL
|
|
623
|
-
AND leased_until > ?
|
|
624
|
-
LIMIT 1
|
|
625
|
-
`).get(projectId, linearIssueId, now);
|
|
626
|
-
const leaseId = typeof row?.lease_id === "string" ? row.lease_id : undefined;
|
|
627
|
-
if (!leaseId)
|
|
628
|
-
return undefined;
|
|
629
|
-
return { projectId, linearIssueId, leaseId };
|
|
630
|
-
}
|
|
631
|
-
withIssueSessionLease(projectId, linearIssueId, leaseId, fn) {
|
|
632
|
-
return this.transaction(() => {
|
|
633
|
-
if (!this.hasActiveIssueSessionLease(projectId, linearIssueId, leaseId)) {
|
|
634
|
-
return undefined;
|
|
635
|
-
}
|
|
636
|
-
return fn();
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
upsertIssueWithLease(lease, params) {
|
|
640
|
-
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => this.upsertIssue(params));
|
|
641
|
-
}
|
|
642
|
-
upsertIssueRespectingActiveLease(projectId, linearIssueId, params) {
|
|
643
|
-
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
644
|
-
if (!lease) {
|
|
645
|
-
return this.upsertIssue(params);
|
|
646
|
-
}
|
|
647
|
-
return this.upsertIssueWithLease(lease, params);
|
|
648
|
-
}
|
|
649
|
-
finishRunWithLease(lease, runId, params) {
|
|
650
|
-
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
651
|
-
this.finishRun(runId, params);
|
|
652
|
-
return true;
|
|
653
|
-
}) ?? false;
|
|
654
|
-
}
|
|
655
|
-
finishRunRespectingActiveLease(projectId, linearIssueId, runId, params) {
|
|
656
|
-
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
657
|
-
if (!lease) {
|
|
658
|
-
this.finishRun(runId, params);
|
|
659
|
-
return true;
|
|
660
|
-
}
|
|
661
|
-
return this.finishRunWithLease(lease, runId, params);
|
|
662
|
-
}
|
|
663
|
-
updateRunThreadWithLease(lease, runId, params) {
|
|
664
|
-
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
665
|
-
this.updateRunThread(runId, params);
|
|
666
|
-
return true;
|
|
667
|
-
}) ?? false;
|
|
668
|
-
}
|
|
669
|
-
consumeIssueSessionEventsWithLease(lease, eventIds, runId) {
|
|
670
|
-
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
671
|
-
this.consumeIssueSessionEvents(lease.projectId, lease.linearIssueId, eventIds, runId);
|
|
672
|
-
return true;
|
|
673
|
-
}) ?? false;
|
|
674
|
-
}
|
|
675
|
-
clearPendingIssueSessionEventsWithLease(lease) {
|
|
676
|
-
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
677
|
-
this.clearPendingIssueSessionEvents(lease.projectId, lease.linearIssueId);
|
|
678
|
-
return true;
|
|
679
|
-
}) ?? false;
|
|
680
|
-
}
|
|
681
|
-
clearPendingIssueSessionEventsRespectingActiveLease(projectId, linearIssueId) {
|
|
682
|
-
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
683
|
-
if (!lease) {
|
|
684
|
-
this.clearPendingIssueSessionEvents(projectId, linearIssueId);
|
|
685
|
-
return true;
|
|
686
|
-
}
|
|
687
|
-
return this.clearPendingIssueSessionEventsWithLease(lease);
|
|
688
|
-
}
|
|
689
|
-
setIssueSessionLastWakeReasonWithLease(lease, lastWakeReason) {
|
|
690
|
-
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
691
|
-
this.setIssueSessionLastWakeReason(lease.projectId, lease.linearIssueId, lastWakeReason);
|
|
692
|
-
return true;
|
|
693
|
-
}) ?? false;
|
|
694
|
-
}
|
|
695
|
-
setIssueSessionLastWakeReason(projectId, linearIssueId, lastWakeReason) {
|
|
696
|
-
this.connection.prepare(`
|
|
697
|
-
UPDATE issue_sessions
|
|
698
|
-
SET last_wake_reason = ?, updated_at = ?
|
|
699
|
-
WHERE project_id = ? AND linear_issue_id = ?
|
|
700
|
-
`).run(lastWakeReason ?? null, isoNow(), projectId, linearIssueId);
|
|
143
|
+
return this.issues.getIssueByPrNumber(prNumber);
|
|
701
144
|
}
|
|
702
145
|
setBranchOwner(projectId, linearIssueId, owner) {
|
|
703
|
-
this.
|
|
704
|
-
UPDATE issues
|
|
705
|
-
SET branch_owner = ?, branch_ownership_changed_at = ?, updated_at = ?
|
|
706
|
-
WHERE project_id = ? AND linear_issue_id = ?
|
|
707
|
-
`).run(owner, isoNow(), isoNow(), projectId, linearIssueId);
|
|
708
|
-
}
|
|
709
|
-
setBranchOwnerWithLease(lease, owner) {
|
|
710
|
-
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
711
|
-
this.setBranchOwner(lease.projectId, lease.linearIssueId, owner);
|
|
712
|
-
return true;
|
|
713
|
-
}) ?? false;
|
|
714
|
-
}
|
|
715
|
-
setBranchOwnerRespectingActiveLease(projectId, linearIssueId, owner) {
|
|
716
|
-
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
717
|
-
if (!lease) {
|
|
718
|
-
this.setBranchOwner(projectId, linearIssueId, owner);
|
|
719
|
-
return true;
|
|
720
|
-
}
|
|
721
|
-
return this.setBranchOwnerWithLease(lease, owner);
|
|
722
|
-
}
|
|
723
|
-
releaseIssueSessionLeaseRespectingActiveLease(projectId, linearIssueId) {
|
|
724
|
-
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
725
|
-
this.releaseIssueSessionLease(projectId, linearIssueId, lease?.leaseId);
|
|
146
|
+
this.issues.setBranchOwner(projectId, linearIssueId, owner);
|
|
726
147
|
}
|
|
727
148
|
replaceIssueDependencies(params) {
|
|
728
|
-
|
|
729
|
-
this.connection
|
|
730
|
-
.prepare("DELETE FROM issue_dependencies WHERE project_id = ? AND linear_issue_id = ?")
|
|
731
|
-
.run(params.projectId, params.linearIssueId);
|
|
732
|
-
if (params.blockers.length === 0) {
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
const insert = this.connection.prepare(`
|
|
736
|
-
INSERT INTO issue_dependencies (
|
|
737
|
-
project_id,
|
|
738
|
-
linear_issue_id,
|
|
739
|
-
blocker_linear_issue_id,
|
|
740
|
-
blocker_issue_key,
|
|
741
|
-
blocker_title,
|
|
742
|
-
blocker_current_linear_state,
|
|
743
|
-
blocker_current_linear_state_type,
|
|
744
|
-
updated_at
|
|
745
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
746
|
-
`);
|
|
747
|
-
for (const blocker of params.blockers) {
|
|
748
|
-
insert.run(params.projectId, params.linearIssueId, blocker.blockerLinearIssueId, blocker.blockerIssueKey ?? null, blocker.blockerTitle ?? null, blocker.blockerCurrentLinearState ?? null, blocker.blockerCurrentLinearStateType ?? null, now);
|
|
749
|
-
}
|
|
149
|
+
this.issues.replaceIssueDependencies(params);
|
|
750
150
|
}
|
|
751
151
|
listIssueDependencies(projectId, linearIssueId) {
|
|
752
|
-
|
|
753
|
-
SELECT
|
|
754
|
-
d.project_id,
|
|
755
|
-
d.linear_issue_id,
|
|
756
|
-
d.blocker_linear_issue_id,
|
|
757
|
-
COALESCE(blockers.issue_key, d.blocker_issue_key) AS blocker_issue_key,
|
|
758
|
-
COALESCE(blockers.title, d.blocker_title) AS blocker_title,
|
|
759
|
-
COALESCE(blockers.current_linear_state, d.blocker_current_linear_state) AS blocker_current_linear_state,
|
|
760
|
-
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type) AS blocker_current_linear_state_type,
|
|
761
|
-
d.updated_at
|
|
762
|
-
FROM issue_dependencies d
|
|
763
|
-
LEFT JOIN issues blockers
|
|
764
|
-
ON blockers.project_id = d.project_id
|
|
765
|
-
AND blockers.linear_issue_id = d.blocker_linear_issue_id
|
|
766
|
-
WHERE d.project_id = ? AND d.linear_issue_id = ?
|
|
767
|
-
ORDER BY COALESCE(blockers.issue_key, d.blocker_issue_key, d.blocker_linear_issue_id) ASC
|
|
768
|
-
`).all(projectId, linearIssueId);
|
|
769
|
-
return rows.map((row) => ({
|
|
770
|
-
projectId: String(row.project_id),
|
|
771
|
-
linearIssueId: String(row.linear_issue_id),
|
|
772
|
-
blockerLinearIssueId: String(row.blocker_linear_issue_id),
|
|
773
|
-
...(row.blocker_issue_key !== null && row.blocker_issue_key !== undefined ? { blockerIssueKey: String(row.blocker_issue_key) } : {}),
|
|
774
|
-
...(row.blocker_title !== null && row.blocker_title !== undefined ? { blockerTitle: String(row.blocker_title) } : {}),
|
|
775
|
-
...(row.blocker_current_linear_state !== null && row.blocker_current_linear_state !== undefined
|
|
776
|
-
? { blockerCurrentLinearState: String(row.blocker_current_linear_state) }
|
|
777
|
-
: {}),
|
|
778
|
-
...(row.blocker_current_linear_state_type !== null && row.blocker_current_linear_state_type !== undefined
|
|
779
|
-
? { blockerCurrentLinearStateType: String(row.blocker_current_linear_state_type) }
|
|
780
|
-
: {}),
|
|
781
|
-
updatedAt: String(row.updated_at),
|
|
782
|
-
}));
|
|
152
|
+
return this.issues.listIssueDependencies(projectId, linearIssueId);
|
|
783
153
|
}
|
|
784
154
|
listDependents(projectId, blockerLinearIssueId) {
|
|
785
|
-
|
|
786
|
-
SELECT project_id, linear_issue_id
|
|
787
|
-
FROM issue_dependencies
|
|
788
|
-
WHERE project_id = ? AND blocker_linear_issue_id = ?
|
|
789
|
-
ORDER BY linear_issue_id ASC
|
|
790
|
-
`).all(projectId, blockerLinearIssueId);
|
|
791
|
-
return rows.map((row) => ({
|
|
792
|
-
projectId: String(row.project_id),
|
|
793
|
-
linearIssueId: String(row.linear_issue_id),
|
|
794
|
-
}));
|
|
155
|
+
return this.issues.listDependents(projectId, blockerLinearIssueId);
|
|
795
156
|
}
|
|
796
157
|
getLatestGitHubCiSnapshot(projectId, linearIssueId) {
|
|
797
|
-
|
|
798
|
-
if (!issue?.lastGitHubCiSnapshotJson)
|
|
799
|
-
return undefined;
|
|
800
|
-
try {
|
|
801
|
-
return JSON.parse(issue.lastGitHubCiSnapshotJson);
|
|
802
|
-
}
|
|
803
|
-
catch {
|
|
804
|
-
return undefined;
|
|
805
|
-
}
|
|
158
|
+
return this.issues.getLatestGitHubCiSnapshot(projectId, linearIssueId);
|
|
806
159
|
}
|
|
807
160
|
countUnresolvedBlockers(projectId, linearIssueId) {
|
|
808
|
-
|
|
809
|
-
SELECT COUNT(*) AS count
|
|
810
|
-
FROM issue_dependencies d
|
|
811
|
-
LEFT JOIN issues blockers
|
|
812
|
-
ON blockers.project_id = d.project_id
|
|
813
|
-
AND blockers.linear_issue_id = d.blocker_linear_issue_id
|
|
814
|
-
WHERE d.project_id = ? AND d.linear_issue_id = ?
|
|
815
|
-
AND (
|
|
816
|
-
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
|
|
817
|
-
AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
|
|
818
|
-
)
|
|
819
|
-
`).get(projectId, linearIssueId);
|
|
820
|
-
return Number(row?.count ?? 0);
|
|
161
|
+
return this.issues.countUnresolvedBlockers(projectId, linearIssueId);
|
|
821
162
|
}
|
|
822
163
|
listIssuesReadyForExecution() {
|
|
823
|
-
return this.listIssues()
|
|
164
|
+
return this.issues.listIssues()
|
|
824
165
|
.filter((issue) => isIssueSessionReadyForExecution({
|
|
825
166
|
factoryState: issue.factoryState,
|
|
826
167
|
sessionState: deriveIssueSessionState({
|
|
@@ -828,8 +169,8 @@ export class PatchRelayDatabase {
|
|
|
828
169
|
factoryState: issue.factoryState,
|
|
829
170
|
}),
|
|
830
171
|
activeRunId: issue.activeRunId,
|
|
831
|
-
blockedByCount: this.countUnresolvedBlockers(issue.projectId, issue.linearIssueId),
|
|
832
|
-
hasPendingWake: this.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined,
|
|
172
|
+
blockedByCount: this.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId),
|
|
173
|
+
hasPendingWake: this.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined,
|
|
833
174
|
hasLegacyPendingRun: issue.pendingRunType !== undefined,
|
|
834
175
|
prNumber: issue.prNumber,
|
|
835
176
|
prState: issue.prState,
|
|
@@ -847,229 +188,35 @@ export class PatchRelayDatabase {
|
|
|
847
188
|
* advancement based on stored PR metadata (missed GitHub webhooks).
|
|
848
189
|
*/
|
|
849
190
|
listIdleNonTerminalIssues() {
|
|
850
|
-
|
|
851
|
-
.prepare(`SELECT * FROM issues
|
|
852
|
-
WHERE factory_state NOT IN ('done', 'escalated', 'failed', 'awaiting_input')
|
|
853
|
-
AND active_run_id IS NULL
|
|
854
|
-
AND pending_run_type IS NULL
|
|
855
|
-
AND pr_number IS NOT NULL`)
|
|
856
|
-
.all();
|
|
857
|
-
return rows.map(mapIssueRow);
|
|
191
|
+
return this.issues.listIdleNonTerminalIssues();
|
|
858
192
|
}
|
|
859
193
|
/**
|
|
860
194
|
* Issues in delegated state with dependencies but no pending/active run.
|
|
861
195
|
* Candidates for unblocking when their blockers complete.
|
|
862
196
|
*/
|
|
863
197
|
listBlockedDelegatedIssues() {
|
|
864
|
-
|
|
865
|
-
.prepare(`SELECT DISTINCT i.* FROM issues i
|
|
866
|
-
JOIN issue_dependencies d ON d.project_id = i.project_id AND d.linear_issue_id = i.linear_issue_id
|
|
867
|
-
WHERE i.factory_state = 'delegated'
|
|
868
|
-
AND i.active_run_id IS NULL
|
|
869
|
-
AND i.pending_run_type IS NULL`)
|
|
870
|
-
.all();
|
|
871
|
-
return rows.map(mapIssueRow);
|
|
198
|
+
return this.issues.listBlockedDelegatedIssues();
|
|
872
199
|
}
|
|
873
200
|
/**
|
|
874
201
|
* Issues waiting in the merge queue with no active or pending run.
|
|
875
202
|
* Used by the queue health monitor to probe GitHub for stuck PRs.
|
|
876
203
|
*/
|
|
877
204
|
listAwaitingQueueIssues() {
|
|
878
|
-
|
|
879
|
-
.prepare(`SELECT * FROM issues
|
|
880
|
-
WHERE factory_state = 'awaiting_queue'
|
|
881
|
-
AND active_run_id IS NULL
|
|
882
|
-
AND pending_run_type IS NULL
|
|
883
|
-
AND pr_number IS NOT NULL`)
|
|
884
|
-
.all();
|
|
885
|
-
return rows.map(mapIssueRow);
|
|
205
|
+
return this.issues.listAwaitingQueueIssues();
|
|
886
206
|
}
|
|
887
207
|
listIssuesByState(projectId, state) {
|
|
888
|
-
|
|
889
|
-
.prepare("SELECT * FROM issues WHERE project_id = ? AND factory_state = ? ORDER BY pr_number ASC")
|
|
890
|
-
.all(projectId, state);
|
|
891
|
-
return rows.map(mapIssueRow);
|
|
892
|
-
}
|
|
893
|
-
// ─── Runs ─────────────────────────────────────────────────────────
|
|
894
|
-
createRun(params) {
|
|
895
|
-
const now = isoNow();
|
|
896
|
-
const result = this.connection.prepare(`
|
|
897
|
-
INSERT INTO runs (issue_id, project_id, linear_issue_id, run_type, status, source_head_sha, prompt_text, started_at)
|
|
898
|
-
VALUES (?, ?, ?, ?, 'queued', ?, ?, ?)
|
|
899
|
-
`).run(params.issueId, params.projectId, params.linearIssueId, params.runType, params.sourceHeadSha ?? null, params.promptText ?? null, now);
|
|
900
|
-
const run = this.getRun(Number(result.lastInsertRowid));
|
|
901
|
-
const issue = this.getIssue(params.projectId, params.linearIssueId);
|
|
902
|
-
if (issue) {
|
|
903
|
-
this.syncIssueSessionFromIssue(issue, { lastRunType: run.runType });
|
|
904
|
-
}
|
|
905
|
-
return run;
|
|
906
|
-
}
|
|
907
|
-
getRun(id) {
|
|
908
|
-
const row = this.connection.prepare("SELECT * FROM runs WHERE id = ?").get(id);
|
|
909
|
-
return row ? mapRunRow(row) : undefined;
|
|
910
|
-
}
|
|
911
|
-
getRunByThreadId(threadId) {
|
|
912
|
-
const row = this.connection.prepare("SELECT * FROM runs WHERE thread_id = ?").get(threadId);
|
|
913
|
-
return row ? mapRunRow(row) : undefined;
|
|
914
|
-
}
|
|
915
|
-
listRunsForIssue(projectId, linearIssueId) {
|
|
916
|
-
const rows = this.connection
|
|
917
|
-
.prepare("SELECT * FROM runs WHERE project_id = ? AND linear_issue_id = ? ORDER BY id")
|
|
918
|
-
.all(projectId, linearIssueId);
|
|
919
|
-
return rows.map(mapRunRow);
|
|
920
|
-
}
|
|
921
|
-
getLatestRunForIssue(projectId, linearIssueId) {
|
|
922
|
-
const row = this.connection
|
|
923
|
-
.prepare("SELECT * FROM runs WHERE project_id = ? AND linear_issue_id = ? ORDER BY id DESC LIMIT 1")
|
|
924
|
-
.get(projectId, linearIssueId);
|
|
925
|
-
return row ? mapRunRow(row) : undefined;
|
|
926
|
-
}
|
|
927
|
-
listActiveRuns() {
|
|
928
|
-
const rows = this.connection
|
|
929
|
-
.prepare("SELECT * FROM runs WHERE status IN ('queued', 'running')")
|
|
930
|
-
.all();
|
|
931
|
-
return rows.map(mapRunRow);
|
|
932
|
-
}
|
|
933
|
-
listRunningRuns() {
|
|
934
|
-
const rows = this.connection
|
|
935
|
-
.prepare("SELECT * FROM runs WHERE status IN ('running', 'queued')")
|
|
936
|
-
.all();
|
|
937
|
-
return rows.map(mapRunRow);
|
|
938
|
-
}
|
|
939
|
-
updateRunThread(runId, params) {
|
|
940
|
-
this.connection.prepare(`
|
|
941
|
-
UPDATE runs SET
|
|
942
|
-
thread_id = ?,
|
|
943
|
-
parent_thread_id = COALESCE(?, parent_thread_id),
|
|
944
|
-
turn_id = COALESCE(?, turn_id),
|
|
945
|
-
status = 'running'
|
|
946
|
-
WHERE id = ?
|
|
947
|
-
AND ended_at IS NULL
|
|
948
|
-
AND status IN ('queued', 'running')
|
|
949
|
-
`).run(params.threadId, params.parentThreadId ?? null, params.turnId ?? null, runId);
|
|
950
|
-
const run = this.getRun(runId);
|
|
951
|
-
if (!run)
|
|
952
|
-
return;
|
|
953
|
-
const issue = this.getIssue(run.projectId, run.linearIssueId);
|
|
954
|
-
if (issue) {
|
|
955
|
-
this.syncIssueSessionFromIssue(issue);
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
updateRunTurnId(runId, turnId) {
|
|
959
|
-
this.connection.prepare("UPDATE runs SET turn_id = ? WHERE id = ?").run(turnId, runId);
|
|
960
|
-
}
|
|
961
|
-
finishRun(runId, params) {
|
|
962
|
-
const now = isoNow();
|
|
963
|
-
this.connection.prepare(`
|
|
964
|
-
UPDATE runs SET
|
|
965
|
-
status = ?,
|
|
966
|
-
thread_id = COALESCE(?, thread_id),
|
|
967
|
-
turn_id = COALESCE(?, turn_id),
|
|
968
|
-
failure_reason = COALESCE(?, failure_reason),
|
|
969
|
-
summary_json = COALESCE(?, summary_json),
|
|
970
|
-
report_json = COALESCE(?, report_json),
|
|
971
|
-
ended_at = ?
|
|
972
|
-
WHERE id = ?
|
|
973
|
-
`).run(params.status, params.threadId ?? null, params.turnId ?? null, params.failureReason ?? null, params.summaryJson ?? null, params.reportJson ?? null, now, runId);
|
|
974
|
-
const run = this.getRun(runId);
|
|
975
|
-
if (!run)
|
|
976
|
-
return;
|
|
977
|
-
const issue = this.getIssue(run.projectId, run.linearIssueId);
|
|
978
|
-
if (issue) {
|
|
979
|
-
this.syncIssueSessionFromIssue(issue, {
|
|
980
|
-
summaryText: extractLatestAssistantSummary(this.getRun(runId) ?? run),
|
|
981
|
-
lastRunType: run.runType,
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
// ─── Thread Events (kept for extended history) ────────────────────
|
|
986
|
-
saveThreadEvent(params) {
|
|
987
|
-
this.connection.prepare(`
|
|
988
|
-
INSERT INTO run_thread_events (run_id, thread_id, turn_id, method, event_json, created_at)
|
|
989
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
990
|
-
`).run(params.runId, params.threadId, params.turnId ?? null, params.method, params.eventJson, isoNow());
|
|
991
|
-
}
|
|
992
|
-
listThreadEvents(runId) {
|
|
993
|
-
const rows = this.connection
|
|
994
|
-
.prepare("SELECT * FROM run_thread_events WHERE run_id = ? ORDER BY id")
|
|
995
|
-
.all(runId);
|
|
996
|
-
return rows.map((row) => ({
|
|
997
|
-
id: Number(row.id),
|
|
998
|
-
runId: Number(row.run_id),
|
|
999
|
-
threadId: String(row.thread_id),
|
|
1000
|
-
...(row.turn_id !== null ? { turnId: String(row.turn_id) } : {}),
|
|
1001
|
-
method: String(row.method),
|
|
1002
|
-
eventJson: String(row.event_json),
|
|
1003
|
-
createdAt: String(row.created_at),
|
|
1004
|
-
}));
|
|
208
|
+
return this.issues.listIssuesByState(projectId, state);
|
|
1005
209
|
}
|
|
1006
210
|
// ─── View builders ──────────────────────────────────────────────
|
|
1007
211
|
issueToTrackedIssue(issue) {
|
|
1008
|
-
|
|
1009
|
-
const blockedBy = this.listIssueDependencies(issue.projectId, issue.linearIssueId);
|
|
1010
|
-
const unresolvedBlockedBy = blockedBy.filter((entry) => !isResolvedLinearState(entry.blockerCurrentLinearStateType, entry.blockerCurrentLinearState));
|
|
1011
|
-
const pendingWake = this.peekIssueSessionWake(issue.projectId, issue.linearIssueId);
|
|
1012
|
-
const failureContext = parseGitHubFailureContext(issue.lastGitHubFailureContextJson);
|
|
1013
|
-
const blockedByKeys = unresolvedBlockedBy.map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId);
|
|
1014
|
-
const waitingReason = derivePatchRelayWaitingReason({
|
|
1015
|
-
...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
|
|
1016
|
-
blockedByKeys,
|
|
1017
|
-
factoryState: issue.factoryState,
|
|
1018
|
-
pendingRunType: issue.pendingRunType,
|
|
1019
|
-
prNumber: issue.prNumber,
|
|
1020
|
-
prHeadSha: issue.prHeadSha,
|
|
1021
|
-
prReviewState: issue.prReviewState,
|
|
1022
|
-
prCheckStatus: issue.prCheckStatus,
|
|
1023
|
-
lastBlockingReviewHeadSha: issue.lastBlockingReviewHeadSha,
|
|
1024
|
-
latestFailureCheckName: issue.lastGitHubFailureCheckName,
|
|
1025
|
-
});
|
|
1026
|
-
const latestRun = this.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
1027
|
-
const latestEvent = this.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1);
|
|
1028
|
-
const statusNote = deriveIssueStatusNote({
|
|
212
|
+
return buildTrackedIssueRecord({
|
|
1029
213
|
issue,
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
waitingReason,
|
|
214
|
+
session: this.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId),
|
|
215
|
+
blockedBy: this.issues.listIssueDependencies(issue.projectId, issue.linearIssueId),
|
|
216
|
+
hasPendingWake: this.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined,
|
|
217
|
+
latestRun: this.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId),
|
|
218
|
+
latestEvent: this.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1),
|
|
1036
219
|
});
|
|
1037
|
-
return {
|
|
1038
|
-
id: issue.id,
|
|
1039
|
-
projectId: issue.projectId,
|
|
1040
|
-
linearIssueId: issue.linearIssueId,
|
|
1041
|
-
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
1042
|
-
...(issue.title ? { title: issue.title } : {}),
|
|
1043
|
-
...(issue.url ? { issueUrl: issue.url } : {}),
|
|
1044
|
-
...(statusNote ? { statusNote } : {}),
|
|
1045
|
-
...(issue.currentLinearState ? { currentLinearState: issue.currentLinearState } : {}),
|
|
1046
|
-
...(session?.sessionState ? { sessionState: session.sessionState } : {}),
|
|
1047
|
-
factoryState: issue.factoryState,
|
|
1048
|
-
blockedByCount: unresolvedBlockedBy.length,
|
|
1049
|
-
blockedByKeys,
|
|
1050
|
-
readyForExecution: isIssueSessionReadyForExecution({
|
|
1051
|
-
sessionState: session?.sessionState,
|
|
1052
|
-
factoryState: issue.factoryState,
|
|
1053
|
-
activeRunId: issue.activeRunId,
|
|
1054
|
-
blockedByCount: unresolvedBlockedBy.length,
|
|
1055
|
-
hasPendingWake: pendingWake !== undefined,
|
|
1056
|
-
hasLegacyPendingRun: issue.pendingRunType !== undefined,
|
|
1057
|
-
...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
|
|
1058
|
-
...(issue.prState ? { prState: issue.prState } : {}),
|
|
1059
|
-
...(issue.prReviewState ? { prReviewState: issue.prReviewState } : {}),
|
|
1060
|
-
...(issue.prCheckStatus ? { prCheckStatus: issue.prCheckStatus } : {}),
|
|
1061
|
-
...(issue.lastGitHubFailureSource ? { latestFailureSource: issue.lastGitHubFailureSource } : {}),
|
|
1062
|
-
}),
|
|
1063
|
-
...(issue.lastGitHubFailureSource ? { latestFailureSource: issue.lastGitHubFailureSource } : {}),
|
|
1064
|
-
...(issue.lastGitHubFailureHeadSha ? { latestFailureHeadSha: issue.lastGitHubFailureHeadSha } : {}),
|
|
1065
|
-
...(issue.lastGitHubFailureCheckName ? { latestFailureCheckName: issue.lastGitHubFailureCheckName } : {}),
|
|
1066
|
-
...(failureContext?.stepName ? { latestFailureStepName: failureContext.stepName } : {}),
|
|
1067
|
-
...(failureContext?.summary ? { latestFailureSummary: failureContext.summary } : {}),
|
|
1068
|
-
...(waitingReason ? { waitingReason } : {}),
|
|
1069
|
-
...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
|
|
1070
|
-
...(issue.agentSessionId ? { activeAgentSessionId: issue.agentSessionId } : {}),
|
|
1071
|
-
updatedAt: issue.updatedAt,
|
|
1072
|
-
};
|
|
1073
220
|
}
|
|
1074
221
|
getTrackedIssue(projectId, linearIssueId) {
|
|
1075
222
|
const issue = this.getIssue(projectId, linearIssueId);
|
|
@@ -1080,42 +227,10 @@ export class PatchRelayDatabase {
|
|
|
1080
227
|
return issue ? this.issueToTrackedIssue(issue) : undefined;
|
|
1081
228
|
}
|
|
1082
229
|
listIssues() {
|
|
1083
|
-
|
|
1084
|
-
.prepare("SELECT * FROM issues ORDER BY updated_at DESC")
|
|
1085
|
-
.all();
|
|
1086
|
-
return rows.map(mapIssueRow);
|
|
230
|
+
return this.issues.listIssues();
|
|
1087
231
|
}
|
|
1088
232
|
listIssuesWithAgentSessions() {
|
|
1089
|
-
|
|
1090
|
-
.prepare("SELECT * FROM issues WHERE agent_session_id IS NOT NULL ORDER BY updated_at DESC")
|
|
1091
|
-
.all();
|
|
1092
|
-
return rows.map(mapIssueRow);
|
|
1093
|
-
}
|
|
1094
|
-
findLatestAgentSessionIdForIssue(linearIssueId) {
|
|
1095
|
-
const row = this.connection.prepare(`
|
|
1096
|
-
SELECT COALESCE(
|
|
1097
|
-
json_extract(payload_json, '$.agentSession.id'),
|
|
1098
|
-
json_extract(payload_json, '$.data.agentSession.id'),
|
|
1099
|
-
json_extract(payload_json, '$.agentSessionId'),
|
|
1100
|
-
json_extract(payload_json, '$.data.agentSessionId')
|
|
1101
|
-
) AS agent_session_id
|
|
1102
|
-
FROM webhook_events
|
|
1103
|
-
WHERE COALESCE(
|
|
1104
|
-
json_extract(payload_json, '$.agentSession.issueId'),
|
|
1105
|
-
json_extract(payload_json, '$.data.agentSession.issueId'),
|
|
1106
|
-
json_extract(payload_json, '$.agentSession.issue.id'),
|
|
1107
|
-
json_extract(payload_json, '$.data.agentSession.issue.id')
|
|
1108
|
-
) = ?
|
|
1109
|
-
AND COALESCE(
|
|
1110
|
-
json_extract(payload_json, '$.agentSession.id'),
|
|
1111
|
-
json_extract(payload_json, '$.data.agentSession.id'),
|
|
1112
|
-
json_extract(payload_json, '$.agentSessionId'),
|
|
1113
|
-
json_extract(payload_json, '$.data.agentSessionId')
|
|
1114
|
-
) IS NOT NULL
|
|
1115
|
-
ORDER BY id DESC
|
|
1116
|
-
LIMIT 1
|
|
1117
|
-
`).get(linearIssueId);
|
|
1118
|
-
return row?.agent_session_id != null ? String(row.agent_session_id) : undefined;
|
|
233
|
+
return this.issues.listIssuesWithAgentSessions();
|
|
1119
234
|
}
|
|
1120
235
|
// ─── Issue overview for query service ─────────────────────────────
|
|
1121
236
|
getIssueOverview(issueKey) {
|
|
@@ -1123,212 +238,14 @@ export class PatchRelayDatabase {
|
|
|
1123
238
|
if (!issue)
|
|
1124
239
|
return undefined;
|
|
1125
240
|
const tracked = this.issueToTrackedIssue(issue);
|
|
1126
|
-
const activeRun = issue.activeRunId ? this.
|
|
241
|
+
const activeRun = issue.activeRunId ? this.runs.getRunById(issue.activeRunId) : undefined;
|
|
1127
242
|
return {
|
|
1128
243
|
issue: tracked,
|
|
1129
244
|
...(activeRun ? { activeRun } : {}),
|
|
1130
245
|
};
|
|
1131
246
|
}
|
|
1132
|
-
syncIssueSessionFromIssue(issue, options) {
|
|
1133
|
-
const tracked = this.issueToTrackedIssue(issue);
|
|
1134
|
-
const existing = this.getIssueSession(issue.projectId, issue.linearIssueId);
|
|
1135
|
-
const latestRun = this.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
1136
|
-
const latestRunType = options?.lastRunType ?? latestRun?.runType ?? existing?.lastRunType;
|
|
1137
|
-
const summaryText = this.resolveIssueSessionSummary(issue, latestRun, existing?.summaryText, options?.summaryText);
|
|
1138
|
-
const activeThreadId = issue.threadId ?? existing?.activeThreadId;
|
|
1139
|
-
const threadGeneration = activeThreadId && activeThreadId !== existing?.activeThreadId
|
|
1140
|
-
? (existing?.threadGeneration ?? 0) + 1
|
|
1141
|
-
: (existing?.threadGeneration ?? (activeThreadId ? 1 : 0));
|
|
1142
|
-
const sessionState = deriveIssueSessionState({
|
|
1143
|
-
...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
|
|
1144
|
-
factoryState: issue.factoryState,
|
|
1145
|
-
});
|
|
1146
|
-
const lastWakeReason = options?.lastWakeReason
|
|
1147
|
-
?? deriveIssueSessionWakeReason({
|
|
1148
|
-
pendingRunType: issue.pendingRunType,
|
|
1149
|
-
factoryState: issue.factoryState,
|
|
1150
|
-
prNumber: issue.prNumber,
|
|
1151
|
-
prState: issue.prState,
|
|
1152
|
-
prReviewState: issue.prReviewState,
|
|
1153
|
-
prCheckStatus: issue.prCheckStatus,
|
|
1154
|
-
latestFailureSource: issue.lastGitHubFailureSource,
|
|
1155
|
-
})
|
|
1156
|
-
?? existing?.lastWakeReason;
|
|
1157
|
-
const now = isoNow();
|
|
1158
|
-
if (existing) {
|
|
1159
|
-
this.connection.prepare(`
|
|
1160
|
-
UPDATE issue_sessions SET
|
|
1161
|
-
issue_key = ?,
|
|
1162
|
-
repo_id = ?,
|
|
1163
|
-
branch_name = ?,
|
|
1164
|
-
worktree_path = ?,
|
|
1165
|
-
pr_number = ?,
|
|
1166
|
-
pr_head_sha = ?,
|
|
1167
|
-
pr_author_login = ?,
|
|
1168
|
-
session_state = ?,
|
|
1169
|
-
waiting_reason = ?,
|
|
1170
|
-
summary_text = ?,
|
|
1171
|
-
active_thread_id = ?,
|
|
1172
|
-
thread_generation = ?,
|
|
1173
|
-
active_run_id = ?,
|
|
1174
|
-
last_run_type = ?,
|
|
1175
|
-
last_wake_reason = ?,
|
|
1176
|
-
ci_repair_attempts = ?,
|
|
1177
|
-
queue_repair_attempts = ?,
|
|
1178
|
-
review_fix_attempts = ?,
|
|
1179
|
-
updated_at = ?
|
|
1180
|
-
WHERE project_id = ? AND linear_issue_id = ?
|
|
1181
|
-
`).run(issue.issueKey ?? null, issue.projectId, issue.branchName ?? null, issue.worktreePath ?? null, issue.prNumber ?? null, issue.prHeadSha ?? null, issue.prAuthorLogin ?? null, sessionState, tracked.waitingReason ?? null, summaryText ?? null, activeThreadId ?? null, threadGeneration, issue.activeRunId ?? null, latestRunType ?? null, lastWakeReason ?? null, issue.ciRepairAttempts, issue.queueRepairAttempts, issue.reviewFixAttempts, now, issue.projectId, issue.linearIssueId);
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
this.connection.prepare(`
|
|
1185
|
-
INSERT INTO issue_sessions (
|
|
1186
|
-
project_id, linear_issue_id, issue_key, repo_id, branch_name, worktree_path,
|
|
1187
|
-
pr_number, pr_head_sha, pr_author_login, session_state, waiting_reason, summary_text,
|
|
1188
|
-
active_thread_id, thread_generation, active_run_id, last_run_type, last_wake_reason,
|
|
1189
|
-
ci_repair_attempts, queue_repair_attempts, review_fix_attempts,
|
|
1190
|
-
created_at, updated_at
|
|
1191
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1192
|
-
`).run(issue.projectId, issue.linearIssueId, issue.issueKey ?? null, issue.projectId, issue.branchName ?? null, issue.worktreePath ?? null, issue.prNumber ?? null, issue.prHeadSha ?? null, issue.prAuthorLogin ?? null, sessionState, tracked.waitingReason ?? null, summaryText ?? null, activeThreadId ?? null, threadGeneration, issue.activeRunId ?? null, latestRunType ?? null, lastWakeReason ?? null, issue.ciRepairAttempts, issue.queueRepairAttempts, issue.reviewFixAttempts, now, now);
|
|
1193
|
-
}
|
|
1194
|
-
resolveIssueSessionSummary(issue, latestRun, existingSummaryText, explicitSummaryText) {
|
|
1195
|
-
if (explicitSummaryText?.trim()) {
|
|
1196
|
-
return explicitSummaryText;
|
|
1197
|
-
}
|
|
1198
|
-
const latestSummary = extractLatestAssistantSummary(latestRun);
|
|
1199
|
-
if (latestRun && (latestRun.status === "queued" || latestRun.status === "running")) {
|
|
1200
|
-
return latestSummary;
|
|
1201
|
-
}
|
|
1202
|
-
if (this.shouldKeepPreviousIssueSummary(issue, latestRun)) {
|
|
1203
|
-
return this.findLatestCompletedRunSummary(issue.projectId, issue.linearIssueId)
|
|
1204
|
-
?? existingSummaryText
|
|
1205
|
-
?? latestSummary;
|
|
1206
|
-
}
|
|
1207
|
-
return latestSummary ?? existingSummaryText;
|
|
1208
|
-
}
|
|
1209
|
-
shouldKeepPreviousIssueSummary(issue, latestRun) {
|
|
1210
|
-
if (!latestRun || latestRun.status !== "failed") {
|
|
1211
|
-
return false;
|
|
1212
|
-
}
|
|
1213
|
-
if (latestRun.summaryJson || latestRun.reportJson) {
|
|
1214
|
-
return false;
|
|
1215
|
-
}
|
|
1216
|
-
return issue.factoryState === "pr_open"
|
|
1217
|
-
|| issue.factoryState === "awaiting_queue"
|
|
1218
|
-
|| issue.factoryState === "done";
|
|
1219
|
-
}
|
|
1220
|
-
findLatestCompletedRunSummary(projectId, linearIssueId) {
|
|
1221
|
-
const runs = this.listRunsForIssue(projectId, linearIssueId);
|
|
1222
|
-
for (let index = runs.length - 1; index >= 0; index -= 1) {
|
|
1223
|
-
const run = runs[index];
|
|
1224
|
-
if (!run || run.status !== "completed") {
|
|
1225
|
-
continue;
|
|
1226
|
-
}
|
|
1227
|
-
const summary = extractLatestAssistantSummary(run);
|
|
1228
|
-
if (summary?.trim()) {
|
|
1229
|
-
return summary;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
return undefined;
|
|
1233
|
-
}
|
|
1234
247
|
}
|
|
1235
248
|
// ─── Row mappers ──────────────────────────────────────────────────
|
|
1236
|
-
function mapIssueRow(row) {
|
|
1237
|
-
return {
|
|
1238
|
-
id: Number(row.id),
|
|
1239
|
-
projectId: String(row.project_id),
|
|
1240
|
-
linearIssueId: String(row.linear_issue_id),
|
|
1241
|
-
...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
|
|
1242
|
-
...(row.title !== null ? { title: String(row.title) } : {}),
|
|
1243
|
-
...(row.description !== null && row.description !== undefined ? { description: String(row.description) } : {}),
|
|
1244
|
-
...(row.url !== null ? { url: String(row.url) } : {}),
|
|
1245
|
-
...(row.priority !== null && row.priority !== undefined ? { priority: Number(row.priority) } : {}),
|
|
1246
|
-
...(row.estimate !== null && row.estimate !== undefined ? { estimate: Number(row.estimate) } : {}),
|
|
1247
|
-
...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
|
|
1248
|
-
...(row.current_linear_state_type !== null && row.current_linear_state_type !== undefined
|
|
1249
|
-
? { currentLinearStateType: String(row.current_linear_state_type) }
|
|
1250
|
-
: {}),
|
|
1251
|
-
factoryState: String(row.factory_state ?? "delegated"),
|
|
1252
|
-
...(row.pending_run_type !== null && row.pending_run_type !== undefined ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
1253
|
-
...(row.pending_run_context_json !== null && row.pending_run_context_json !== undefined ? { pendingRunContextJson: String(row.pending_run_context_json) } : {}),
|
|
1254
|
-
...(row.branch_name !== null ? { branchName: String(row.branch_name) } : {}),
|
|
1255
|
-
...(row.branch_owner !== null && row.branch_owner !== undefined && String(row.branch_owner) === "patchrelay"
|
|
1256
|
-
? { branchOwner: "patchrelay" }
|
|
1257
|
-
: { branchOwner: "patchrelay" }),
|
|
1258
|
-
...(row.branch_ownership_changed_at !== null && row.branch_ownership_changed_at !== undefined
|
|
1259
|
-
? { branchOwnershipChangedAt: String(row.branch_ownership_changed_at) }
|
|
1260
|
-
: {}),
|
|
1261
|
-
...(row.worktree_path !== null ? { worktreePath: String(row.worktree_path) } : {}),
|
|
1262
|
-
...(row.thread_id !== null ? { threadId: String(row.thread_id) } : {}),
|
|
1263
|
-
...(row.active_run_id !== null ? { activeRunId: Number(row.active_run_id) } : {}),
|
|
1264
|
-
...(row.status_comment_id !== null && row.status_comment_id !== undefined ? { statusCommentId: String(row.status_comment_id) } : {}),
|
|
1265
|
-
...(row.agent_session_id !== null ? { agentSessionId: String(row.agent_session_id) } : {}),
|
|
1266
|
-
updatedAt: String(row.updated_at),
|
|
1267
|
-
...(row.pr_number !== null && row.pr_number !== undefined ? { prNumber: Number(row.pr_number) } : {}),
|
|
1268
|
-
...(row.pr_url !== null && row.pr_url !== undefined ? { prUrl: String(row.pr_url) } : {}),
|
|
1269
|
-
...(row.pr_state !== null && row.pr_state !== undefined ? { prState: String(row.pr_state) } : {}),
|
|
1270
|
-
...(row.pr_head_sha !== null && row.pr_head_sha !== undefined ? { prHeadSha: String(row.pr_head_sha) } : {}),
|
|
1271
|
-
...(row.pr_author_login !== null && row.pr_author_login !== undefined ? { prAuthorLogin: String(row.pr_author_login) } : {}),
|
|
1272
|
-
...(row.pr_review_state !== null && row.pr_review_state !== undefined ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
1273
|
-
...(row.pr_check_status !== null && row.pr_check_status !== undefined ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
1274
|
-
...(row.last_blocking_review_head_sha !== null && row.last_blocking_review_head_sha !== undefined
|
|
1275
|
-
? { lastBlockingReviewHeadSha: String(row.last_blocking_review_head_sha) }
|
|
1276
|
-
: {}),
|
|
1277
|
-
...(row.last_github_failure_source !== null && row.last_github_failure_source !== undefined
|
|
1278
|
-
? { lastGitHubFailureSource: String(row.last_github_failure_source) }
|
|
1279
|
-
: {}),
|
|
1280
|
-
...(row.last_github_failure_head_sha !== null && row.last_github_failure_head_sha !== undefined
|
|
1281
|
-
? { lastGitHubFailureHeadSha: String(row.last_github_failure_head_sha) }
|
|
1282
|
-
: {}),
|
|
1283
|
-
...(row.last_github_failure_signature !== null && row.last_github_failure_signature !== undefined
|
|
1284
|
-
? { lastGitHubFailureSignature: String(row.last_github_failure_signature) }
|
|
1285
|
-
: {}),
|
|
1286
|
-
...(row.last_github_failure_check_name !== null && row.last_github_failure_check_name !== undefined
|
|
1287
|
-
? { lastGitHubFailureCheckName: String(row.last_github_failure_check_name) }
|
|
1288
|
-
: {}),
|
|
1289
|
-
...(row.last_github_failure_check_url !== null && row.last_github_failure_check_url !== undefined
|
|
1290
|
-
? { lastGitHubFailureCheckUrl: String(row.last_github_failure_check_url) }
|
|
1291
|
-
: {}),
|
|
1292
|
-
...(row.last_github_failure_context_json !== null && row.last_github_failure_context_json !== undefined
|
|
1293
|
-
? { lastGitHubFailureContextJson: String(row.last_github_failure_context_json) }
|
|
1294
|
-
: {}),
|
|
1295
|
-
...(row.last_github_failure_at !== null && row.last_github_failure_at !== undefined
|
|
1296
|
-
? { lastGitHubFailureAt: String(row.last_github_failure_at) }
|
|
1297
|
-
: {}),
|
|
1298
|
-
...(row.last_github_ci_snapshot_head_sha !== null && row.last_github_ci_snapshot_head_sha !== undefined
|
|
1299
|
-
? { lastGitHubCiSnapshotHeadSha: String(row.last_github_ci_snapshot_head_sha) }
|
|
1300
|
-
: {}),
|
|
1301
|
-
...(row.last_github_ci_snapshot_gate_check_name !== null && row.last_github_ci_snapshot_gate_check_name !== undefined
|
|
1302
|
-
? { lastGitHubCiSnapshotGateCheckName: String(row.last_github_ci_snapshot_gate_check_name) }
|
|
1303
|
-
: {}),
|
|
1304
|
-
...(row.last_github_ci_snapshot_gate_check_status !== null && row.last_github_ci_snapshot_gate_check_status !== undefined
|
|
1305
|
-
? { lastGitHubCiSnapshotGateCheckStatus: String(row.last_github_ci_snapshot_gate_check_status) }
|
|
1306
|
-
: {}),
|
|
1307
|
-
...(row.last_github_ci_snapshot_json !== null && row.last_github_ci_snapshot_json !== undefined
|
|
1308
|
-
? { lastGitHubCiSnapshotJson: String(row.last_github_ci_snapshot_json) }
|
|
1309
|
-
: {}),
|
|
1310
|
-
...(row.last_github_ci_snapshot_settled_at !== null && row.last_github_ci_snapshot_settled_at !== undefined
|
|
1311
|
-
? { lastGitHubCiSnapshotSettledAt: String(row.last_github_ci_snapshot_settled_at) }
|
|
1312
|
-
: {}),
|
|
1313
|
-
...(row.last_queue_signal_at !== null && row.last_queue_signal_at !== undefined
|
|
1314
|
-
? { lastQueueSignalAt: String(row.last_queue_signal_at) }
|
|
1315
|
-
: {}),
|
|
1316
|
-
...(row.last_queue_incident_json !== null && row.last_queue_incident_json !== undefined
|
|
1317
|
-
? { lastQueueIncidentJson: String(row.last_queue_incident_json) }
|
|
1318
|
-
: {}),
|
|
1319
|
-
...(row.last_attempted_failure_head_sha !== null && row.last_attempted_failure_head_sha !== undefined
|
|
1320
|
-
? { lastAttemptedFailureHeadSha: String(row.last_attempted_failure_head_sha) }
|
|
1321
|
-
: {}),
|
|
1322
|
-
...(row.last_attempted_failure_signature !== null && row.last_attempted_failure_signature !== undefined
|
|
1323
|
-
? { lastAttemptedFailureSignature: String(row.last_attempted_failure_signature) }
|
|
1324
|
-
: {}),
|
|
1325
|
-
ciRepairAttempts: Number(row.ci_repair_attempts ?? 0),
|
|
1326
|
-
queueRepairAttempts: Number(row.queue_repair_attempts ?? 0),
|
|
1327
|
-
reviewFixAttempts: Number(row.review_fix_attempts ?? 0),
|
|
1328
|
-
zombieRecoveryAttempts: Number(row.zombie_recovery_attempts ?? 0),
|
|
1329
|
-
...(row.last_zombie_recovery_at !== null && row.last_zombie_recovery_at !== undefined ? { lastZombieRecoveryAt: String(row.last_zombie_recovery_at) } : {}),
|
|
1330
|
-
};
|
|
1331
|
-
}
|
|
1332
249
|
function mapIssueSessionRow(row) {
|
|
1333
250
|
return {
|
|
1334
251
|
id: Number(row.id),
|
|
@@ -1392,6 +309,3 @@ function mapRunRow(row) {
|
|
|
1392
309
|
...(row.ended_at !== null ? { endedAt: String(row.ended_at) } : {}),
|
|
1393
310
|
};
|
|
1394
311
|
}
|
|
1395
|
-
function isResolvedLinearState(stateType, stateName) {
|
|
1396
|
-
return stateType === "completed" || stateName?.trim().toLowerCase() === "done";
|
|
1397
|
-
}
|