patchrelay 0.36.8 → 0.36.10

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/db.js CHANGED
@@ -1,5 +1,6 @@
1
- import { isIssueSessionReadyForExecution, deriveIssueSessionState, deriveIssueSessionReactiveIntent, deriveIssueSessionWakeReason, } from "./issue-session.js";
2
- import { extractLatestAssistantSummary, } from "./issue-session-events.js";
1
+ import { isIssueSessionReadyForExecution, deriveIssueSessionState, deriveIssueSessionReactiveIntent, } from "./issue-session.js";
2
+ import {} from "./issue-session-events.js";
3
+ import { IssueStore } from "./db/issue-store.js";
3
4
  import { IssueSessionStore } from "./db/issue-session-store.js";
4
5
  import { LinearInstallationStore } from "./db/linear-installation-store.js";
5
6
  import { OperatorFeedStore } from "./db/operator-feed-store.js";
@@ -7,7 +8,8 @@ import { RepositoryLinkStore } from "./db/repository-link-store.js";
7
8
  import { RunStore } from "./db/run-store.js";
8
9
  import { WebhookEventStore } from "./db/webhook-event-store.js";
9
10
  import { runPatchRelayMigrations } from "./db/migrations.js";
10
- import { SqliteConnection, isoNow } from "./db/shared.js";
11
+ import { SqliteConnection } from "./db/shared.js";
12
+ import { syncIssueSessionFromIssue } from "./issue-session-projector.js";
11
13
  import { buildTrackedIssueRecord } from "./tracked-issue-projector.js";
12
14
  function parseObjectJson(raw) {
13
15
  if (!raw)
@@ -92,6 +94,7 @@ export class PatchRelayDatabase {
92
94
  operatorFeed;
93
95
  repositories;
94
96
  webhookEvents;
97
+ issues;
95
98
  issueSessions;
96
99
  runs;
97
100
  constructor(databasePath, wal) {
@@ -104,8 +107,16 @@ export class PatchRelayDatabase {
104
107
  this.operatorFeed = new OperatorFeedStore(this.connection);
105
108
  this.repositories = new RepositoryLinkStore(this.connection);
106
109
  this.webhookEvents = new WebhookEventStore(this.connection);
107
- this.issueSessions = new IssueSessionStore(this.connection, mapIssueSessionRow, mapIssueSessionEventRow, (projectId, linearIssueId) => this.getIssue(projectId, linearIssueId), deriveImplicitReactiveWake, (fn) => this.transaction(fn), (params) => this.upsertIssue(params), (runId, params) => this.runs.finishRun(runId, params), (runId, params) => this.runs.updateRunThread(runId, params), (projectId, linearIssueId, owner) => this.setBranchOwner(projectId, linearIssueId, owner));
108
- this.runs = new RunStore(this.connection, mapRunRow, (id) => this.runs.getRunById(id), (projectId, linearIssueId) => this.getIssue(projectId, linearIssueId), (issue, options) => this.syncIssueSessionFromIssue(issue, options));
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);
109
120
  }
110
121
  runMigrations() {
111
122
  runPatchRelayMigrations(this.connection);
@@ -113,415 +124,44 @@ export class PatchRelayDatabase {
113
124
  transaction(fn) {
114
125
  return this.connection.transaction(fn)();
115
126
  }
116
- // ─── Issues ───────────────────────────────────────────────────────
117
127
  upsertIssue(params) {
118
- const now = isoNow();
119
- const existing = this.getIssue(params.projectId, params.linearIssueId);
120
- if (existing) {
121
- // Build dynamic SET clauses for nullable fields
122
- const sets = ["updated_at = @now"];
123
- const values = {
124
- now,
125
- projectId: params.projectId,
126
- linearIssueId: params.linearIssueId,
127
- };
128
- if (params.issueKey !== undefined) {
129
- sets.push("issue_key = COALESCE(@issueKey, issue_key)");
130
- values.issueKey = params.issueKey;
131
- }
132
- if (params.title !== undefined) {
133
- sets.push("title = COALESCE(@title, title)");
134
- values.title = params.title;
135
- }
136
- if (params.description !== undefined) {
137
- sets.push("description = COALESCE(@description, description)");
138
- values.description = params.description;
139
- }
140
- if (params.url !== undefined) {
141
- sets.push("url = COALESCE(@url, url)");
142
- values.url = params.url;
143
- }
144
- if (params.priority !== undefined) {
145
- sets.push("priority = @priority");
146
- values.priority = params.priority;
147
- }
148
- if (params.estimate !== undefined) {
149
- sets.push("estimate = @estimate");
150
- values.estimate = params.estimate;
151
- }
152
- if (params.currentLinearState !== undefined) {
153
- sets.push("current_linear_state = COALESCE(@currentLinearState, current_linear_state)");
154
- values.currentLinearState = params.currentLinearState;
155
- }
156
- if (params.currentLinearStateType !== undefined) {
157
- sets.push("current_linear_state_type = COALESCE(@currentLinearStateType, current_linear_state_type)");
158
- values.currentLinearStateType = params.currentLinearStateType;
159
- }
160
- if (params.factoryState !== undefined) {
161
- sets.push("factory_state = @factoryState");
162
- values.factoryState = params.factoryState;
163
- }
164
- if (params.pendingRunType !== undefined) {
165
- sets.push("pending_run_type = @pendingRunType");
166
- values.pendingRunType = params.pendingRunType;
167
- }
168
- if (params.pendingRunContextJson !== undefined) {
169
- sets.push("pending_run_context_json = @pendingRunContextJson");
170
- values.pendingRunContextJson = params.pendingRunContextJson;
171
- }
172
- if (params.branchName !== undefined) {
173
- sets.push("branch_name = COALESCE(@branchName, branch_name)");
174
- values.branchName = params.branchName;
175
- }
176
- if (params.worktreePath !== undefined) {
177
- sets.push("worktree_path = COALESCE(@worktreePath, worktree_path)");
178
- values.worktreePath = params.worktreePath;
179
- }
180
- if (params.threadId !== undefined) {
181
- sets.push("thread_id = @threadId");
182
- values.threadId = params.threadId;
183
- }
184
- if (params.activeRunId !== undefined) {
185
- sets.push("active_run_id = @activeRunId");
186
- values.activeRunId = params.activeRunId;
187
- }
188
- if (params.statusCommentId !== undefined) {
189
- sets.push("status_comment_id = @statusCommentId");
190
- values.statusCommentId = params.statusCommentId;
191
- }
192
- if (params.agentSessionId !== undefined) {
193
- sets.push("agent_session_id = @agentSessionId");
194
- values.agentSessionId = params.agentSessionId;
195
- }
196
- if (params.prNumber !== undefined) {
197
- sets.push("pr_number = @prNumber");
198
- values.prNumber = params.prNumber;
199
- }
200
- if (params.prUrl !== undefined) {
201
- sets.push("pr_url = @prUrl");
202
- values.prUrl = params.prUrl;
203
- }
204
- if (params.prState !== undefined) {
205
- sets.push("pr_state = @prState");
206
- values.prState = params.prState;
207
- }
208
- if (params.prHeadSha !== undefined) {
209
- sets.push("pr_head_sha = @prHeadSha");
210
- values.prHeadSha = params.prHeadSha;
211
- }
212
- if (params.prAuthorLogin !== undefined) {
213
- sets.push("pr_author_login = @prAuthorLogin");
214
- values.prAuthorLogin = params.prAuthorLogin;
215
- }
216
- if (params.prReviewState !== undefined) {
217
- sets.push("pr_review_state = @prReviewState");
218
- values.prReviewState = params.prReviewState;
219
- }
220
- if (params.prCheckStatus !== undefined) {
221
- sets.push("pr_check_status = @prCheckStatus");
222
- values.prCheckStatus = params.prCheckStatus;
223
- }
224
- if (params.lastBlockingReviewHeadSha !== undefined) {
225
- sets.push("last_blocking_review_head_sha = @lastBlockingReviewHeadSha");
226
- values.lastBlockingReviewHeadSha = params.lastBlockingReviewHeadSha;
227
- }
228
- if (params.lastGitHubFailureSource !== undefined) {
229
- sets.push("last_github_failure_source = @lastGitHubFailureSource");
230
- values.lastGitHubFailureSource = params.lastGitHubFailureSource;
231
- }
232
- if (params.lastGitHubFailureHeadSha !== undefined) {
233
- sets.push("last_github_failure_head_sha = @lastGitHubFailureHeadSha");
234
- values.lastGitHubFailureHeadSha = params.lastGitHubFailureHeadSha;
235
- }
236
- if (params.lastGitHubFailureSignature !== undefined) {
237
- sets.push("last_github_failure_signature = @lastGitHubFailureSignature");
238
- values.lastGitHubFailureSignature = params.lastGitHubFailureSignature;
239
- }
240
- if (params.lastGitHubFailureCheckName !== undefined) {
241
- sets.push("last_github_failure_check_name = @lastGitHubFailureCheckName");
242
- values.lastGitHubFailureCheckName = params.lastGitHubFailureCheckName;
243
- }
244
- if (params.lastGitHubFailureCheckUrl !== undefined) {
245
- sets.push("last_github_failure_check_url = @lastGitHubFailureCheckUrl");
246
- values.lastGitHubFailureCheckUrl = params.lastGitHubFailureCheckUrl;
247
- }
248
- if (params.lastGitHubFailureContextJson !== undefined) {
249
- sets.push("last_github_failure_context_json = @lastGitHubFailureContextJson");
250
- values.lastGitHubFailureContextJson = params.lastGitHubFailureContextJson;
251
- }
252
- if (params.lastGitHubFailureAt !== undefined) {
253
- sets.push("last_github_failure_at = @lastGitHubFailureAt");
254
- values.lastGitHubFailureAt = params.lastGitHubFailureAt;
255
- }
256
- if (params.lastGitHubCiSnapshotHeadSha !== undefined) {
257
- sets.push("last_github_ci_snapshot_head_sha = @lastGitHubCiSnapshotHeadSha");
258
- values.lastGitHubCiSnapshotHeadSha = params.lastGitHubCiSnapshotHeadSha;
259
- }
260
- if (params.lastGitHubCiSnapshotGateCheckName !== undefined) {
261
- sets.push("last_github_ci_snapshot_gate_check_name = @lastGitHubCiSnapshotGateCheckName");
262
- values.lastGitHubCiSnapshotGateCheckName = params.lastGitHubCiSnapshotGateCheckName;
263
- }
264
- if (params.lastGitHubCiSnapshotGateCheckStatus !== undefined) {
265
- sets.push("last_github_ci_snapshot_gate_check_status = @lastGitHubCiSnapshotGateCheckStatus");
266
- values.lastGitHubCiSnapshotGateCheckStatus = params.lastGitHubCiSnapshotGateCheckStatus;
267
- }
268
- if (params.lastGitHubCiSnapshotJson !== undefined) {
269
- sets.push("last_github_ci_snapshot_json = @lastGitHubCiSnapshotJson");
270
- values.lastGitHubCiSnapshotJson = params.lastGitHubCiSnapshotJson;
271
- }
272
- if (params.lastGitHubCiSnapshotSettledAt !== undefined) {
273
- sets.push("last_github_ci_snapshot_settled_at = @lastGitHubCiSnapshotSettledAt");
274
- values.lastGitHubCiSnapshotSettledAt = params.lastGitHubCiSnapshotSettledAt;
275
- }
276
- if (params.lastQueueSignalAt !== undefined) {
277
- sets.push("last_queue_signal_at = @lastQueueSignalAt");
278
- values.lastQueueSignalAt = params.lastQueueSignalAt;
279
- }
280
- if (params.lastQueueIncidentJson !== undefined) {
281
- sets.push("last_queue_incident_json = @lastQueueIncidentJson");
282
- values.lastQueueIncidentJson = params.lastQueueIncidentJson;
283
- }
284
- if (params.lastAttemptedFailureHeadSha !== undefined) {
285
- sets.push("last_attempted_failure_head_sha = @lastAttemptedFailureHeadSha");
286
- values.lastAttemptedFailureHeadSha = params.lastAttemptedFailureHeadSha;
287
- }
288
- if (params.lastAttemptedFailureSignature !== undefined) {
289
- sets.push("last_attempted_failure_signature = @lastAttemptedFailureSignature");
290
- values.lastAttemptedFailureSignature = params.lastAttemptedFailureSignature;
291
- }
292
- if (params.ciRepairAttempts !== undefined) {
293
- sets.push("ci_repair_attempts = @ciRepairAttempts");
294
- values.ciRepairAttempts = params.ciRepairAttempts;
295
- }
296
- if (params.queueRepairAttempts !== undefined) {
297
- sets.push("queue_repair_attempts = @queueRepairAttempts");
298
- values.queueRepairAttempts = params.queueRepairAttempts;
299
- }
300
- if (params.reviewFixAttempts !== undefined) {
301
- sets.push("review_fix_attempts = @reviewFixAttempts");
302
- values.reviewFixAttempts = params.reviewFixAttempts;
303
- }
304
- if (params.zombieRecoveryAttempts !== undefined) {
305
- sets.push("zombie_recovery_attempts = @zombieRecoveryAttempts");
306
- values.zombieRecoveryAttempts = params.zombieRecoveryAttempts;
307
- }
308
- if (params.lastZombieRecoveryAt !== undefined) {
309
- sets.push("last_zombie_recovery_at = @lastZombieRecoveryAt");
310
- values.lastZombieRecoveryAt = params.lastZombieRecoveryAt;
311
- }
312
- this.connection.prepare(`UPDATE issues SET ${sets.join(", ")} WHERE project_id = @projectId AND linear_issue_id = @linearIssueId`).run(values);
313
- }
314
- else {
315
- this.connection.prepare(`
316
- INSERT INTO issues (
317
- project_id, linear_issue_id, issue_key, title, description, url,
318
- priority, estimate,
319
- current_linear_state, current_linear_state_type, factory_state, pending_run_type, pending_run_context_json,
320
- branch_name, worktree_path, thread_id, active_run_id, status_comment_id,
321
- agent_session_id,
322
- pr_number, pr_url, pr_state, pr_head_sha, pr_author_login, pr_review_state, pr_check_status, last_blocking_review_head_sha,
323
- 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,
324
- 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,
325
- last_queue_signal_at, last_queue_incident_json,
326
- last_attempted_failure_head_sha, last_attempted_failure_signature,
327
- ci_repair_attempts, queue_repair_attempts, review_fix_attempts, zombie_recovery_attempts, last_zombie_recovery_at,
328
- updated_at
329
- ) VALUES (
330
- @projectId, @linearIssueId, @issueKey, @title, @description, @url,
331
- @priority, @estimate,
332
- @currentLinearState, @currentLinearStateType, @factoryState, @pendingRunType, @pendingRunContextJson,
333
- @branchName, @worktreePath, @threadId, @activeRunId, @statusCommentId,
334
- @agentSessionId,
335
- @prNumber, @prUrl, @prState, @prHeadSha, @prAuthorLogin, @prReviewState, @prCheckStatus, @lastBlockingReviewHeadSha,
336
- @lastGitHubFailureSource, @lastGitHubFailureHeadSha, @lastGitHubFailureSignature, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureContextJson, @lastGitHubFailureAt,
337
- @lastGitHubCiSnapshotHeadSha, @lastGitHubCiSnapshotGateCheckName, @lastGitHubCiSnapshotGateCheckStatus, @lastGitHubCiSnapshotJson, @lastGitHubCiSnapshotSettledAt,
338
- @lastQueueSignalAt, @lastQueueIncidentJson,
339
- @lastAttemptedFailureHeadSha, @lastAttemptedFailureSignature,
340
- @ciRepairAttempts, @queueRepairAttempts, @reviewFixAttempts, @zombieRecoveryAttempts, @lastZombieRecoveryAt,
341
- @now
342
- )
343
- `).run({
344
- projectId: params.projectId,
345
- linearIssueId: params.linearIssueId,
346
- issueKey: params.issueKey ?? null,
347
- title: params.title ?? null,
348
- description: params.description ?? null,
349
- url: params.url ?? null,
350
- priority: params.priority ?? null,
351
- estimate: params.estimate ?? null,
352
- currentLinearState: params.currentLinearState ?? null,
353
- currentLinearStateType: params.currentLinearStateType ?? null,
354
- factoryState: params.factoryState ?? "delegated",
355
- pendingRunType: params.pendingRunType ?? null,
356
- pendingRunContextJson: params.pendingRunContextJson ?? null,
357
- branchName: params.branchName ?? null,
358
- worktreePath: params.worktreePath ?? null,
359
- threadId: params.threadId ?? null,
360
- activeRunId: params.activeRunId ?? null,
361
- statusCommentId: params.statusCommentId ?? null,
362
- agentSessionId: params.agentSessionId ?? null,
363
- prNumber: params.prNumber ?? null,
364
- prUrl: params.prUrl ?? null,
365
- prState: params.prState ?? null,
366
- prHeadSha: params.prHeadSha ?? null,
367
- prAuthorLogin: params.prAuthorLogin ?? null,
368
- prReviewState: params.prReviewState ?? null,
369
- prCheckStatus: params.prCheckStatus ?? null,
370
- lastBlockingReviewHeadSha: params.lastBlockingReviewHeadSha ?? null,
371
- lastGitHubFailureSource: params.lastGitHubFailureSource ?? null,
372
- lastGitHubFailureHeadSha: params.lastGitHubFailureHeadSha ?? null,
373
- lastGitHubFailureSignature: params.lastGitHubFailureSignature ?? null,
374
- lastGitHubFailureCheckName: params.lastGitHubFailureCheckName ?? null,
375
- lastGitHubFailureCheckUrl: params.lastGitHubFailureCheckUrl ?? null,
376
- lastGitHubFailureContextJson: params.lastGitHubFailureContextJson ?? null,
377
- lastGitHubFailureAt: params.lastGitHubFailureAt ?? null,
378
- lastGitHubCiSnapshotHeadSha: params.lastGitHubCiSnapshotHeadSha ?? null,
379
- lastGitHubCiSnapshotGateCheckName: params.lastGitHubCiSnapshotGateCheckName ?? null,
380
- lastGitHubCiSnapshotGateCheckStatus: params.lastGitHubCiSnapshotGateCheckStatus ?? null,
381
- lastGitHubCiSnapshotJson: params.lastGitHubCiSnapshotJson ?? null,
382
- lastGitHubCiSnapshotSettledAt: params.lastGitHubCiSnapshotSettledAt ?? null,
383
- lastQueueSignalAt: params.lastQueueSignalAt ?? null,
384
- lastQueueIncidentJson: params.lastQueueIncidentJson ?? null,
385
- lastAttemptedFailureHeadSha: params.lastAttemptedFailureHeadSha ?? null,
386
- lastAttemptedFailureSignature: params.lastAttemptedFailureSignature ?? null,
387
- ciRepairAttempts: params.ciRepairAttempts ?? 0,
388
- queueRepairAttempts: params.queueRepairAttempts ?? 0,
389
- reviewFixAttempts: params.reviewFixAttempts ?? 0,
390
- zombieRecoveryAttempts: params.zombieRecoveryAttempts ?? 0,
391
- lastZombieRecoveryAt: params.lastZombieRecoveryAt ?? null,
392
- now,
393
- });
394
- }
395
- const updated = this.getIssue(params.projectId, params.linearIssueId);
396
- this.syncIssueSessionFromIssue(updated);
397
- return updated;
128
+ return this.issues.upsertIssue(params);
398
129
  }
399
130
  getIssue(projectId, linearIssueId) {
400
- const row = this.connection
401
- .prepare("SELECT * FROM issues WHERE project_id = ? AND linear_issue_id = ?")
402
- .get(projectId, linearIssueId);
403
- return row ? mapIssueRow(row) : undefined;
131
+ return this.issues.getIssue(projectId, linearIssueId);
404
132
  }
405
133
  getIssueById(id) {
406
- const row = this.connection.prepare("SELECT * FROM issues WHERE id = ?").get(id);
407
- return row ? mapIssueRow(row) : undefined;
134
+ return this.issues.getIssueById(id);
408
135
  }
409
136
  getIssueByKey(issueKey) {
410
- const row = this.connection.prepare("SELECT * FROM issues WHERE issue_key = ?").get(issueKey);
411
- return row ? mapIssueRow(row) : undefined;
137
+ return this.issues.getIssueByKey(issueKey);
412
138
  }
413
139
  getIssueByBranch(branchName) {
414
- const row = this.connection.prepare("SELECT * FROM issues WHERE branch_name = ?").get(branchName);
415
- return row ? mapIssueRow(row) : undefined;
140
+ return this.issues.getIssueByBranch(branchName);
416
141
  }
417
142
  getIssueByPrNumber(prNumber) {
418
- const row = this.connection.prepare("SELECT * FROM issues WHERE pr_number = ?").get(prNumber);
419
- return row ? mapIssueRow(row) : undefined;
143
+ return this.issues.getIssueByPrNumber(prNumber);
420
144
  }
421
145
  setBranchOwner(projectId, linearIssueId, owner) {
422
- this.connection.prepare(`
423
- UPDATE issues
424
- SET branch_owner = ?, branch_ownership_changed_at = ?, updated_at = ?
425
- WHERE project_id = ? AND linear_issue_id = ?
426
- `).run(owner, isoNow(), isoNow(), projectId, linearIssueId);
146
+ this.issues.setBranchOwner(projectId, linearIssueId, owner);
427
147
  }
428
148
  replaceIssueDependencies(params) {
429
- const now = isoNow();
430
- this.connection
431
- .prepare("DELETE FROM issue_dependencies WHERE project_id = ? AND linear_issue_id = ?")
432
- .run(params.projectId, params.linearIssueId);
433
- if (params.blockers.length === 0) {
434
- return;
435
- }
436
- const insert = this.connection.prepare(`
437
- INSERT INTO issue_dependencies (
438
- project_id,
439
- linear_issue_id,
440
- blocker_linear_issue_id,
441
- blocker_issue_key,
442
- blocker_title,
443
- blocker_current_linear_state,
444
- blocker_current_linear_state_type,
445
- updated_at
446
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
447
- `);
448
- for (const blocker of params.blockers) {
449
- insert.run(params.projectId, params.linearIssueId, blocker.blockerLinearIssueId, blocker.blockerIssueKey ?? null, blocker.blockerTitle ?? null, blocker.blockerCurrentLinearState ?? null, blocker.blockerCurrentLinearStateType ?? null, now);
450
- }
149
+ this.issues.replaceIssueDependencies(params);
451
150
  }
452
151
  listIssueDependencies(projectId, linearIssueId) {
453
- const rows = this.connection.prepare(`
454
- SELECT
455
- d.project_id,
456
- d.linear_issue_id,
457
- d.blocker_linear_issue_id,
458
- COALESCE(blockers.issue_key, d.blocker_issue_key) AS blocker_issue_key,
459
- COALESCE(blockers.title, d.blocker_title) AS blocker_title,
460
- COALESCE(blockers.current_linear_state, d.blocker_current_linear_state) AS blocker_current_linear_state,
461
- COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type) AS blocker_current_linear_state_type,
462
- d.updated_at
463
- FROM issue_dependencies d
464
- LEFT JOIN issues blockers
465
- ON blockers.project_id = d.project_id
466
- AND blockers.linear_issue_id = d.blocker_linear_issue_id
467
- WHERE d.project_id = ? AND d.linear_issue_id = ?
468
- ORDER BY COALESCE(blockers.issue_key, d.blocker_issue_key, d.blocker_linear_issue_id) ASC
469
- `).all(projectId, linearIssueId);
470
- return rows.map((row) => ({
471
- projectId: String(row.project_id),
472
- linearIssueId: String(row.linear_issue_id),
473
- blockerLinearIssueId: String(row.blocker_linear_issue_id),
474
- ...(row.blocker_issue_key !== null && row.blocker_issue_key !== undefined ? { blockerIssueKey: String(row.blocker_issue_key) } : {}),
475
- ...(row.blocker_title !== null && row.blocker_title !== undefined ? { blockerTitle: String(row.blocker_title) } : {}),
476
- ...(row.blocker_current_linear_state !== null && row.blocker_current_linear_state !== undefined
477
- ? { blockerCurrentLinearState: String(row.blocker_current_linear_state) }
478
- : {}),
479
- ...(row.blocker_current_linear_state_type !== null && row.blocker_current_linear_state_type !== undefined
480
- ? { blockerCurrentLinearStateType: String(row.blocker_current_linear_state_type) }
481
- : {}),
482
- updatedAt: String(row.updated_at),
483
- }));
152
+ return this.issues.listIssueDependencies(projectId, linearIssueId);
484
153
  }
485
154
  listDependents(projectId, blockerLinearIssueId) {
486
- const rows = this.connection.prepare(`
487
- SELECT project_id, linear_issue_id
488
- FROM issue_dependencies
489
- WHERE project_id = ? AND blocker_linear_issue_id = ?
490
- ORDER BY linear_issue_id ASC
491
- `).all(projectId, blockerLinearIssueId);
492
- return rows.map((row) => ({
493
- projectId: String(row.project_id),
494
- linearIssueId: String(row.linear_issue_id),
495
- }));
155
+ return this.issues.listDependents(projectId, blockerLinearIssueId);
496
156
  }
497
157
  getLatestGitHubCiSnapshot(projectId, linearIssueId) {
498
- const issue = this.getIssue(projectId, linearIssueId);
499
- if (!issue?.lastGitHubCiSnapshotJson)
500
- return undefined;
501
- try {
502
- return JSON.parse(issue.lastGitHubCiSnapshotJson);
503
- }
504
- catch {
505
- return undefined;
506
- }
158
+ return this.issues.getLatestGitHubCiSnapshot(projectId, linearIssueId);
507
159
  }
508
160
  countUnresolvedBlockers(projectId, linearIssueId) {
509
- const row = this.connection.prepare(`
510
- SELECT COUNT(*) AS count
511
- FROM issue_dependencies d
512
- LEFT JOIN issues blockers
513
- ON blockers.project_id = d.project_id
514
- AND blockers.linear_issue_id = d.blocker_linear_issue_id
515
- WHERE d.project_id = ? AND d.linear_issue_id = ?
516
- AND (
517
- COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
518
- AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
519
- )
520
- `).get(projectId, linearIssueId);
521
- return Number(row?.count ?? 0);
161
+ return this.issues.countUnresolvedBlockers(projectId, linearIssueId);
522
162
  }
523
163
  listIssuesReadyForExecution() {
524
- return this.listIssues()
164
+ return this.issues.listIssues()
525
165
  .filter((issue) => isIssueSessionReadyForExecution({
526
166
  factoryState: issue.factoryState,
527
167
  sessionState: deriveIssueSessionState({
@@ -529,7 +169,7 @@ export class PatchRelayDatabase {
529
169
  factoryState: issue.factoryState,
530
170
  }),
531
171
  activeRunId: issue.activeRunId,
532
- blockedByCount: this.countUnresolvedBlockers(issue.projectId, issue.linearIssueId),
172
+ blockedByCount: this.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId),
533
173
  hasPendingWake: this.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined,
534
174
  hasLegacyPendingRun: issue.pendingRunType !== undefined,
535
175
  prNumber: issue.prNumber,
@@ -548,55 +188,31 @@ export class PatchRelayDatabase {
548
188
  * advancement based on stored PR metadata (missed GitHub webhooks).
549
189
  */
550
190
  listIdleNonTerminalIssues() {
551
- const rows = this.connection
552
- .prepare(`SELECT * FROM issues
553
- WHERE factory_state NOT IN ('done', 'escalated', 'failed', 'awaiting_input')
554
- AND active_run_id IS NULL
555
- AND pending_run_type IS NULL
556
- AND pr_number IS NOT NULL`)
557
- .all();
558
- return rows.map(mapIssueRow);
191
+ return this.issues.listIdleNonTerminalIssues();
559
192
  }
560
193
  /**
561
194
  * Issues in delegated state with dependencies but no pending/active run.
562
195
  * Candidates for unblocking when their blockers complete.
563
196
  */
564
197
  listBlockedDelegatedIssues() {
565
- const rows = this.connection
566
- .prepare(`SELECT DISTINCT i.* FROM issues i
567
- JOIN issue_dependencies d ON d.project_id = i.project_id AND d.linear_issue_id = i.linear_issue_id
568
- WHERE i.factory_state = 'delegated'
569
- AND i.active_run_id IS NULL
570
- AND i.pending_run_type IS NULL`)
571
- .all();
572
- return rows.map(mapIssueRow);
198
+ return this.issues.listBlockedDelegatedIssues();
573
199
  }
574
200
  /**
575
201
  * Issues waiting in the merge queue with no active or pending run.
576
202
  * Used by the queue health monitor to probe GitHub for stuck PRs.
577
203
  */
578
204
  listAwaitingQueueIssues() {
579
- const rows = this.connection
580
- .prepare(`SELECT * FROM issues
581
- WHERE factory_state = 'awaiting_queue'
582
- AND active_run_id IS NULL
583
- AND pending_run_type IS NULL
584
- AND pr_number IS NOT NULL`)
585
- .all();
586
- return rows.map(mapIssueRow);
205
+ return this.issues.listAwaitingQueueIssues();
587
206
  }
588
207
  listIssuesByState(projectId, state) {
589
- const rows = this.connection
590
- .prepare("SELECT * FROM issues WHERE project_id = ? AND factory_state = ? ORDER BY pr_number ASC")
591
- .all(projectId, state);
592
- return rows.map(mapIssueRow);
208
+ return this.issues.listIssuesByState(projectId, state);
593
209
  }
594
210
  // ─── View builders ──────────────────────────────────────────────
595
211
  issueToTrackedIssue(issue) {
596
212
  return buildTrackedIssueRecord({
597
213
  issue,
598
214
  session: this.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId),
599
- blockedBy: this.listIssueDependencies(issue.projectId, issue.linearIssueId),
215
+ blockedBy: this.issues.listIssueDependencies(issue.projectId, issue.linearIssueId),
600
216
  hasPendingWake: this.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined,
601
217
  latestRun: this.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId),
602
218
  latestEvent: this.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1),
@@ -611,16 +227,10 @@ export class PatchRelayDatabase {
611
227
  return issue ? this.issueToTrackedIssue(issue) : undefined;
612
228
  }
613
229
  listIssues() {
614
- const rows = this.connection
615
- .prepare("SELECT * FROM issues ORDER BY updated_at DESC")
616
- .all();
617
- return rows.map(mapIssueRow);
230
+ return this.issues.listIssues();
618
231
  }
619
232
  listIssuesWithAgentSessions() {
620
- const rows = this.connection
621
- .prepare("SELECT * FROM issues WHERE agent_session_id IS NOT NULL ORDER BY updated_at DESC")
622
- .all();
623
- return rows.map(mapIssueRow);
233
+ return this.issues.listIssuesWithAgentSessions();
624
234
  }
625
235
  // ─── Issue overview for query service ─────────────────────────────
626
236
  getIssueOverview(issueKey) {
@@ -634,206 +244,8 @@ export class PatchRelayDatabase {
634
244
  ...(activeRun ? { activeRun } : {}),
635
245
  };
636
246
  }
637
- syncIssueSessionFromIssue(issue, options) {
638
- const tracked = this.issueToTrackedIssue(issue);
639
- const existing = this.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId);
640
- const latestRun = this.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
641
- const latestRunType = options?.lastRunType ?? latestRun?.runType ?? existing?.lastRunType;
642
- const summaryText = this.resolveIssueSessionSummary(issue, latestRun, existing?.summaryText, options?.summaryText);
643
- const activeThreadId = issue.threadId ?? existing?.activeThreadId;
644
- const threadGeneration = activeThreadId && activeThreadId !== existing?.activeThreadId
645
- ? (existing?.threadGeneration ?? 0) + 1
646
- : (existing?.threadGeneration ?? (activeThreadId ? 1 : 0));
647
- const sessionState = deriveIssueSessionState({
648
- ...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
649
- factoryState: issue.factoryState,
650
- });
651
- const lastWakeReason = options?.lastWakeReason
652
- ?? deriveIssueSessionWakeReason({
653
- pendingRunType: issue.pendingRunType,
654
- factoryState: issue.factoryState,
655
- prNumber: issue.prNumber,
656
- prState: issue.prState,
657
- prReviewState: issue.prReviewState,
658
- prCheckStatus: issue.prCheckStatus,
659
- latestFailureSource: issue.lastGitHubFailureSource,
660
- })
661
- ?? existing?.lastWakeReason;
662
- const now = isoNow();
663
- if (existing) {
664
- this.connection.prepare(`
665
- UPDATE issue_sessions SET
666
- issue_key = ?,
667
- repo_id = ?,
668
- branch_name = ?,
669
- worktree_path = ?,
670
- pr_number = ?,
671
- pr_head_sha = ?,
672
- pr_author_login = ?,
673
- session_state = ?,
674
- waiting_reason = ?,
675
- summary_text = ?,
676
- active_thread_id = ?,
677
- thread_generation = ?,
678
- active_run_id = ?,
679
- last_run_type = ?,
680
- last_wake_reason = ?,
681
- ci_repair_attempts = ?,
682
- queue_repair_attempts = ?,
683
- review_fix_attempts = ?,
684
- updated_at = ?
685
- WHERE project_id = ? AND linear_issue_id = ?
686
- `).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);
687
- return;
688
- }
689
- this.connection.prepare(`
690
- INSERT INTO issue_sessions (
691
- project_id, linear_issue_id, issue_key, repo_id, branch_name, worktree_path,
692
- pr_number, pr_head_sha, pr_author_login, session_state, waiting_reason, summary_text,
693
- active_thread_id, thread_generation, active_run_id, last_run_type, last_wake_reason,
694
- ci_repair_attempts, queue_repair_attempts, review_fix_attempts,
695
- created_at, updated_at
696
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
697
- `).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);
698
- }
699
- resolveIssueSessionSummary(issue, latestRun, existingSummaryText, explicitSummaryText) {
700
- if (explicitSummaryText?.trim()) {
701
- return explicitSummaryText;
702
- }
703
- const latestSummary = extractLatestAssistantSummary(latestRun);
704
- if (latestRun && (latestRun.status === "queued" || latestRun.status === "running")) {
705
- return latestSummary;
706
- }
707
- if (this.shouldKeepPreviousIssueSummary(issue, latestRun)) {
708
- return this.findLatestCompletedRunSummary(issue.projectId, issue.linearIssueId)
709
- ?? existingSummaryText
710
- ?? latestSummary;
711
- }
712
- return latestSummary ?? existingSummaryText;
713
- }
714
- shouldKeepPreviousIssueSummary(issue, latestRun) {
715
- if (!latestRun || latestRun.status !== "failed") {
716
- return false;
717
- }
718
- if (latestRun.summaryJson || latestRun.reportJson) {
719
- return false;
720
- }
721
- return issue.factoryState === "pr_open"
722
- || issue.factoryState === "awaiting_queue"
723
- || issue.factoryState === "done";
724
- }
725
- findLatestCompletedRunSummary(projectId, linearIssueId) {
726
- const runs = this.runs.listRunsForIssue(projectId, linearIssueId);
727
- for (let index = runs.length - 1; index >= 0; index -= 1) {
728
- const run = runs[index];
729
- if (!run || run.status !== "completed") {
730
- continue;
731
- }
732
- const summary = extractLatestAssistantSummary(run);
733
- if (summary?.trim()) {
734
- return summary;
735
- }
736
- }
737
- return undefined;
738
- }
739
247
  }
740
248
  // ─── Row mappers ──────────────────────────────────────────────────
741
- function mapIssueRow(row) {
742
- return {
743
- id: Number(row.id),
744
- projectId: String(row.project_id),
745
- linearIssueId: String(row.linear_issue_id),
746
- ...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
747
- ...(row.title !== null ? { title: String(row.title) } : {}),
748
- ...(row.description !== null && row.description !== undefined ? { description: String(row.description) } : {}),
749
- ...(row.url !== null ? { url: String(row.url) } : {}),
750
- ...(row.priority !== null && row.priority !== undefined ? { priority: Number(row.priority) } : {}),
751
- ...(row.estimate !== null && row.estimate !== undefined ? { estimate: Number(row.estimate) } : {}),
752
- ...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
753
- ...(row.current_linear_state_type !== null && row.current_linear_state_type !== undefined
754
- ? { currentLinearStateType: String(row.current_linear_state_type) }
755
- : {}),
756
- factoryState: String(row.factory_state ?? "delegated"),
757
- ...(row.pending_run_type !== null && row.pending_run_type !== undefined ? { pendingRunType: String(row.pending_run_type) } : {}),
758
- ...(row.pending_run_context_json !== null && row.pending_run_context_json !== undefined ? { pendingRunContextJson: String(row.pending_run_context_json) } : {}),
759
- ...(row.branch_name !== null ? { branchName: String(row.branch_name) } : {}),
760
- ...(row.branch_owner !== null && row.branch_owner !== undefined && String(row.branch_owner) === "patchrelay"
761
- ? { branchOwner: "patchrelay" }
762
- : { branchOwner: "patchrelay" }),
763
- ...(row.branch_ownership_changed_at !== null && row.branch_ownership_changed_at !== undefined
764
- ? { branchOwnershipChangedAt: String(row.branch_ownership_changed_at) }
765
- : {}),
766
- ...(row.worktree_path !== null ? { worktreePath: String(row.worktree_path) } : {}),
767
- ...(row.thread_id !== null ? { threadId: String(row.thread_id) } : {}),
768
- ...(row.active_run_id !== null ? { activeRunId: Number(row.active_run_id) } : {}),
769
- ...(row.status_comment_id !== null && row.status_comment_id !== undefined ? { statusCommentId: String(row.status_comment_id) } : {}),
770
- ...(row.agent_session_id !== null ? { agentSessionId: String(row.agent_session_id) } : {}),
771
- updatedAt: String(row.updated_at),
772
- ...(row.pr_number !== null && row.pr_number !== undefined ? { prNumber: Number(row.pr_number) } : {}),
773
- ...(row.pr_url !== null && row.pr_url !== undefined ? { prUrl: String(row.pr_url) } : {}),
774
- ...(row.pr_state !== null && row.pr_state !== undefined ? { prState: String(row.pr_state) } : {}),
775
- ...(row.pr_head_sha !== null && row.pr_head_sha !== undefined ? { prHeadSha: String(row.pr_head_sha) } : {}),
776
- ...(row.pr_author_login !== null && row.pr_author_login !== undefined ? { prAuthorLogin: String(row.pr_author_login) } : {}),
777
- ...(row.pr_review_state !== null && row.pr_review_state !== undefined ? { prReviewState: String(row.pr_review_state) } : {}),
778
- ...(row.pr_check_status !== null && row.pr_check_status !== undefined ? { prCheckStatus: String(row.pr_check_status) } : {}),
779
- ...(row.last_blocking_review_head_sha !== null && row.last_blocking_review_head_sha !== undefined
780
- ? { lastBlockingReviewHeadSha: String(row.last_blocking_review_head_sha) }
781
- : {}),
782
- ...(row.last_github_failure_source !== null && row.last_github_failure_source !== undefined
783
- ? { lastGitHubFailureSource: String(row.last_github_failure_source) }
784
- : {}),
785
- ...(row.last_github_failure_head_sha !== null && row.last_github_failure_head_sha !== undefined
786
- ? { lastGitHubFailureHeadSha: String(row.last_github_failure_head_sha) }
787
- : {}),
788
- ...(row.last_github_failure_signature !== null && row.last_github_failure_signature !== undefined
789
- ? { lastGitHubFailureSignature: String(row.last_github_failure_signature) }
790
- : {}),
791
- ...(row.last_github_failure_check_name !== null && row.last_github_failure_check_name !== undefined
792
- ? { lastGitHubFailureCheckName: String(row.last_github_failure_check_name) }
793
- : {}),
794
- ...(row.last_github_failure_check_url !== null && row.last_github_failure_check_url !== undefined
795
- ? { lastGitHubFailureCheckUrl: String(row.last_github_failure_check_url) }
796
- : {}),
797
- ...(row.last_github_failure_context_json !== null && row.last_github_failure_context_json !== undefined
798
- ? { lastGitHubFailureContextJson: String(row.last_github_failure_context_json) }
799
- : {}),
800
- ...(row.last_github_failure_at !== null && row.last_github_failure_at !== undefined
801
- ? { lastGitHubFailureAt: String(row.last_github_failure_at) }
802
- : {}),
803
- ...(row.last_github_ci_snapshot_head_sha !== null && row.last_github_ci_snapshot_head_sha !== undefined
804
- ? { lastGitHubCiSnapshotHeadSha: String(row.last_github_ci_snapshot_head_sha) }
805
- : {}),
806
- ...(row.last_github_ci_snapshot_gate_check_name !== null && row.last_github_ci_snapshot_gate_check_name !== undefined
807
- ? { lastGitHubCiSnapshotGateCheckName: String(row.last_github_ci_snapshot_gate_check_name) }
808
- : {}),
809
- ...(row.last_github_ci_snapshot_gate_check_status !== null && row.last_github_ci_snapshot_gate_check_status !== undefined
810
- ? { lastGitHubCiSnapshotGateCheckStatus: String(row.last_github_ci_snapshot_gate_check_status) }
811
- : {}),
812
- ...(row.last_github_ci_snapshot_json !== null && row.last_github_ci_snapshot_json !== undefined
813
- ? { lastGitHubCiSnapshotJson: String(row.last_github_ci_snapshot_json) }
814
- : {}),
815
- ...(row.last_github_ci_snapshot_settled_at !== null && row.last_github_ci_snapshot_settled_at !== undefined
816
- ? { lastGitHubCiSnapshotSettledAt: String(row.last_github_ci_snapshot_settled_at) }
817
- : {}),
818
- ...(row.last_queue_signal_at !== null && row.last_queue_signal_at !== undefined
819
- ? { lastQueueSignalAt: String(row.last_queue_signal_at) }
820
- : {}),
821
- ...(row.last_queue_incident_json !== null && row.last_queue_incident_json !== undefined
822
- ? { lastQueueIncidentJson: String(row.last_queue_incident_json) }
823
- : {}),
824
- ...(row.last_attempted_failure_head_sha !== null && row.last_attempted_failure_head_sha !== undefined
825
- ? { lastAttemptedFailureHeadSha: String(row.last_attempted_failure_head_sha) }
826
- : {}),
827
- ...(row.last_attempted_failure_signature !== null && row.last_attempted_failure_signature !== undefined
828
- ? { lastAttemptedFailureSignature: String(row.last_attempted_failure_signature) }
829
- : {}),
830
- ciRepairAttempts: Number(row.ci_repair_attempts ?? 0),
831
- queueRepairAttempts: Number(row.queue_repair_attempts ?? 0),
832
- reviewFixAttempts: Number(row.review_fix_attempts ?? 0),
833
- zombieRecoveryAttempts: Number(row.zombie_recovery_attempts ?? 0),
834
- ...(row.last_zombie_recovery_at !== null && row.last_zombie_recovery_at !== undefined ? { lastZombieRecoveryAt: String(row.last_zombie_recovery_at) } : {}),
835
- };
836
- }
837
249
  function mapIssueSessionRow(row) {
838
250
  return {
839
251
  id: Number(row.id),