patchrelay 0.68.0 → 0.68.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "service": "patchrelay",
3
- "version": "0.68.0",
4
- "commit": "3a86b2f7100f",
5
- "builtAt": "2026-05-13T20:52:25.195Z"
3
+ "version": "0.68.2",
4
+ "commit": "396b8e7a3462",
5
+ "builtAt": "2026-05-15T11:40:22.942Z"
6
6
  }
@@ -1,3 +1,4 @@
1
+ import { buildInsertBindings, buildUpdateAssignments } from "./issue-upsert-columns.js";
1
2
  import { isoNow } from "./shared.js";
2
3
  export class IssueStore {
3
4
  connection;
@@ -10,355 +11,22 @@ export class IssueStore {
10
11
  const now = isoNow();
11
12
  const existing = this.getIssue(params.projectId, params.linearIssueId);
12
13
  if (existing) {
13
- const sets = ["updated_at = @now"];
14
- const values = {
14
+ const { assignments, values } = buildUpdateAssignments(params);
15
+ const sql = `UPDATE issues SET ${["updated_at = @now", ...assignments].join(", ")} WHERE project_id = @projectId AND linear_issue_id = @linearIssueId`;
16
+ this.connection.prepare(sql).run({
17
+ ...values,
15
18
  now,
16
19
  projectId: params.projectId,
17
20
  linearIssueId: params.linearIssueId,
18
- };
19
- if (params.delegatedToPatchRelay !== undefined) {
20
- sets.push("delegated_to_patchrelay = @delegatedToPatchRelay");
21
- values.delegatedToPatchRelay = params.delegatedToPatchRelay ? 1 : 0;
22
- }
23
- if (params.issueClass !== undefined) {
24
- sets.push("issue_class = @issueClass");
25
- values.issueClass = params.issueClass;
26
- }
27
- if (params.issueClassSource !== undefined) {
28
- sets.push("issue_class_source = @issueClassSource");
29
- values.issueClassSource = params.issueClassSource;
30
- }
31
- if (params.issueTriageHash !== undefined) {
32
- sets.push("issue_triage_hash = @issueTriageHash");
33
- values.issueTriageHash = params.issueTriageHash;
34
- }
35
- if (params.issueTriageResultJson !== undefined) {
36
- sets.push("issue_triage_result_json = @issueTriageResultJson");
37
- values.issueTriageResultJson = params.issueTriageResultJson;
38
- }
39
- if (params.parentLinearIssueId !== undefined) {
40
- sets.push("parent_linear_issue_id = @parentLinearIssueId");
41
- values.parentLinearIssueId = params.parentLinearIssueId;
42
- }
43
- if (params.parentIssueKey !== undefined) {
44
- sets.push("parent_issue_key = @parentIssueKey");
45
- values.parentIssueKey = params.parentIssueKey;
46
- }
47
- if (params.issueKey !== undefined) {
48
- sets.push("issue_key = COALESCE(@issueKey, issue_key)");
49
- values.issueKey = params.issueKey;
50
- }
51
- if (params.title !== undefined) {
52
- sets.push("title = COALESCE(@title, title)");
53
- values.title = params.title;
54
- }
55
- if (params.description !== undefined) {
56
- sets.push("description = COALESCE(@description, description)");
57
- values.description = params.description;
58
- }
59
- if (params.url !== undefined) {
60
- sets.push("url = COALESCE(@url, url)");
61
- values.url = params.url;
62
- }
63
- if (params.priority !== undefined) {
64
- sets.push("priority = @priority");
65
- values.priority = params.priority;
66
- }
67
- if (params.estimate !== undefined) {
68
- sets.push("estimate = @estimate");
69
- values.estimate = params.estimate;
70
- }
71
- if (params.currentLinearState !== undefined) {
72
- sets.push("current_linear_state = COALESCE(@currentLinearState, current_linear_state)");
73
- values.currentLinearState = params.currentLinearState;
74
- }
75
- if (params.currentLinearStateType !== undefined) {
76
- sets.push("current_linear_state_type = COALESCE(@currentLinearStateType, current_linear_state_type)");
77
- values.currentLinearStateType = params.currentLinearStateType;
78
- }
79
- if (params.factoryState !== undefined) {
80
- sets.push("factory_state = @factoryState");
81
- values.factoryState = params.factoryState;
82
- }
83
- if (params.pendingRunType !== undefined) {
84
- sets.push("pending_run_type = @pendingRunType");
85
- values.pendingRunType = params.pendingRunType;
86
- }
87
- if (params.pendingRunContextJson !== undefined) {
88
- sets.push("pending_run_context_json = @pendingRunContextJson");
89
- values.pendingRunContextJson = params.pendingRunContextJson;
90
- }
91
- if (params.branchName !== undefined) {
92
- sets.push("branch_name = COALESCE(@branchName, branch_name)");
93
- values.branchName = params.branchName;
94
- }
95
- if (params.worktreePath !== undefined) {
96
- sets.push("worktree_path = COALESCE(@worktreePath, worktree_path)");
97
- values.worktreePath = params.worktreePath;
98
- }
99
- if (params.threadId !== undefined) {
100
- sets.push("thread_id = @threadId");
101
- values.threadId = params.threadId;
102
- }
103
- if (params.activeRunId !== undefined) {
104
- sets.push("active_run_id = @activeRunId");
105
- values.activeRunId = params.activeRunId;
106
- }
107
- if (params.statusCommentId !== undefined) {
108
- sets.push("status_comment_id = @statusCommentId");
109
- values.statusCommentId = params.statusCommentId;
110
- }
111
- if (params.agentSessionId !== undefined) {
112
- sets.push("agent_session_id = @agentSessionId");
113
- values.agentSessionId = params.agentSessionId;
114
- }
115
- if (params.lastLinearActivityKey !== undefined) {
116
- sets.push("last_linear_activity_key = @lastLinearActivityKey");
117
- values.lastLinearActivityKey = params.lastLinearActivityKey;
118
- }
119
- if (params.prNumber !== undefined) {
120
- sets.push("pr_number = @prNumber");
121
- values.prNumber = params.prNumber;
122
- }
123
- if (params.prUrl !== undefined) {
124
- sets.push("pr_url = @prUrl");
125
- values.prUrl = params.prUrl;
126
- }
127
- if (params.prState !== undefined) {
128
- sets.push("pr_state = @prState");
129
- values.prState = params.prState;
130
- }
131
- if (params.prIsDraft !== undefined) {
132
- sets.push("pr_is_draft = @prIsDraft");
133
- values.prIsDraft = params.prIsDraft == null ? null : params.prIsDraft ? 1 : 0;
134
- }
135
- if (params.prHeadSha !== undefined) {
136
- sets.push("pr_head_sha = @prHeadSha");
137
- values.prHeadSha = params.prHeadSha;
138
- }
139
- if (params.prAuthorLogin !== undefined) {
140
- sets.push("pr_author_login = @prAuthorLogin");
141
- values.prAuthorLogin = params.prAuthorLogin;
142
- }
143
- if (params.prReviewState !== undefined) {
144
- sets.push("pr_review_state = @prReviewState");
145
- values.prReviewState = params.prReviewState;
146
- }
147
- if (params.prCheckStatus !== undefined) {
148
- sets.push("pr_check_status = @prCheckStatus");
149
- values.prCheckStatus = params.prCheckStatus;
150
- }
151
- if (params.lastBlockingReviewHeadSha !== undefined) {
152
- sets.push("last_blocking_review_head_sha = @lastBlockingReviewHeadSha");
153
- values.lastBlockingReviewHeadSha = params.lastBlockingReviewHeadSha;
154
- }
155
- if (params.lastGitHubFailureSource !== undefined) {
156
- sets.push("last_github_failure_source = @lastGitHubFailureSource");
157
- values.lastGitHubFailureSource = params.lastGitHubFailureSource;
158
- }
159
- if (params.lastGitHubFailureHeadSha !== undefined) {
160
- sets.push("last_github_failure_head_sha = @lastGitHubFailureHeadSha");
161
- values.lastGitHubFailureHeadSha = params.lastGitHubFailureHeadSha;
162
- }
163
- if (params.lastGitHubFailureSignature !== undefined) {
164
- sets.push("last_github_failure_signature = @lastGitHubFailureSignature");
165
- values.lastGitHubFailureSignature = params.lastGitHubFailureSignature;
166
- }
167
- if (params.lastGitHubFailureCheckName !== undefined) {
168
- sets.push("last_github_failure_check_name = @lastGitHubFailureCheckName");
169
- values.lastGitHubFailureCheckName = params.lastGitHubFailureCheckName;
170
- }
171
- if (params.lastGitHubFailureCheckUrl !== undefined) {
172
- sets.push("last_github_failure_check_url = @lastGitHubFailureCheckUrl");
173
- values.lastGitHubFailureCheckUrl = params.lastGitHubFailureCheckUrl;
174
- }
175
- if (params.lastGitHubFailureContextJson !== undefined) {
176
- sets.push("last_github_failure_context_json = @lastGitHubFailureContextJson");
177
- values.lastGitHubFailureContextJson = params.lastGitHubFailureContextJson;
178
- }
179
- if (params.lastGitHubFailureAt !== undefined) {
180
- sets.push("last_github_failure_at = @lastGitHubFailureAt");
181
- values.lastGitHubFailureAt = params.lastGitHubFailureAt;
182
- }
183
- if (params.lastGitHubCiSnapshotHeadSha !== undefined) {
184
- sets.push("last_github_ci_snapshot_head_sha = @lastGitHubCiSnapshotHeadSha");
185
- values.lastGitHubCiSnapshotHeadSha = params.lastGitHubCiSnapshotHeadSha;
186
- }
187
- if (params.lastGitHubCiSnapshotGateCheckName !== undefined) {
188
- sets.push("last_github_ci_snapshot_gate_check_name = @lastGitHubCiSnapshotGateCheckName");
189
- values.lastGitHubCiSnapshotGateCheckName = params.lastGitHubCiSnapshotGateCheckName;
190
- }
191
- if (params.lastGitHubCiSnapshotGateCheckStatus !== undefined) {
192
- sets.push("last_github_ci_snapshot_gate_check_status = @lastGitHubCiSnapshotGateCheckStatus");
193
- values.lastGitHubCiSnapshotGateCheckStatus = params.lastGitHubCiSnapshotGateCheckStatus;
194
- }
195
- if (params.lastGitHubCiSnapshotJson !== undefined) {
196
- sets.push("last_github_ci_snapshot_json = @lastGitHubCiSnapshotJson");
197
- values.lastGitHubCiSnapshotJson = params.lastGitHubCiSnapshotJson;
198
- }
199
- if (params.lastGitHubCiSnapshotSettledAt !== undefined) {
200
- sets.push("last_github_ci_snapshot_settled_at = @lastGitHubCiSnapshotSettledAt");
201
- values.lastGitHubCiSnapshotSettledAt = params.lastGitHubCiSnapshotSettledAt;
202
- }
203
- if (params.lastQueueSignalAt !== undefined) {
204
- sets.push("last_queue_signal_at = @lastQueueSignalAt");
205
- values.lastQueueSignalAt = params.lastQueueSignalAt;
206
- }
207
- if (params.lastQueueIncidentJson !== undefined) {
208
- sets.push("last_queue_incident_json = @lastQueueIncidentJson");
209
- values.lastQueueIncidentJson = params.lastQueueIncidentJson;
210
- }
211
- if (params.lastAttemptedFailureHeadSha !== undefined) {
212
- sets.push("last_attempted_failure_head_sha = @lastAttemptedFailureHeadSha");
213
- values.lastAttemptedFailureHeadSha = params.lastAttemptedFailureHeadSha;
214
- }
215
- if (params.lastAttemptedFailureSignature !== undefined) {
216
- sets.push("last_attempted_failure_signature = @lastAttemptedFailureSignature");
217
- values.lastAttemptedFailureSignature = params.lastAttemptedFailureSignature;
218
- }
219
- if (params.lastAttemptedFailureAt !== undefined) {
220
- sets.push("last_attempted_failure_at = @lastAttemptedFailureAt");
221
- values.lastAttemptedFailureAt = params.lastAttemptedFailureAt;
222
- }
223
- if (params.lastPublishedPatchId !== undefined) {
224
- sets.push("last_published_patch_id = @lastPublishedPatchId");
225
- values.lastPublishedPatchId = params.lastPublishedPatchId;
226
- }
227
- if (params.lastPublishedIntegrationTreeId !== undefined) {
228
- sets.push("last_published_integration_tree_id = @lastPublishedIntegrationTreeId");
229
- values.lastPublishedIntegrationTreeId = params.lastPublishedIntegrationTreeId;
230
- }
231
- if (params.lastPublishedHeadSha !== undefined) {
232
- sets.push("last_published_head_sha = @lastPublishedHeadSha");
233
- values.lastPublishedHeadSha = params.lastPublishedHeadSha;
234
- }
235
- if (params.parentPrBranch !== undefined) {
236
- sets.push("parent_pr_branch = @parentPrBranch");
237
- values.parentPrBranch = params.parentPrBranch;
238
- }
239
- if (params.ciRepairAttempts !== undefined) {
240
- sets.push("ci_repair_attempts = @ciRepairAttempts");
241
- values.ciRepairAttempts = params.ciRepairAttempts;
242
- }
243
- if (params.queueRepairAttempts !== undefined) {
244
- sets.push("queue_repair_attempts = @queueRepairAttempts");
245
- values.queueRepairAttempts = params.queueRepairAttempts;
246
- }
247
- if (params.reviewFixAttempts !== undefined) {
248
- sets.push("review_fix_attempts = @reviewFixAttempts");
249
- values.reviewFixAttempts = params.reviewFixAttempts;
250
- }
251
- if (params.zombieRecoveryAttempts !== undefined) {
252
- sets.push("zombie_recovery_attempts = @zombieRecoveryAttempts");
253
- values.zombieRecoveryAttempts = params.zombieRecoveryAttempts;
254
- }
255
- if (params.lastZombieRecoveryAt !== undefined) {
256
- sets.push("last_zombie_recovery_at = @lastZombieRecoveryAt");
257
- values.lastZombieRecoveryAt = params.lastZombieRecoveryAt;
258
- }
259
- if (params.orchestrationSettleUntil !== undefined) {
260
- sets.push("orchestration_settle_until = @orchestrationSettleUntil");
261
- values.orchestrationSettleUntil = params.orchestrationSettleUntil;
262
- }
263
- this.connection.prepare(`UPDATE issues SET ${sets.join(", ")} WHERE project_id = @projectId AND linear_issue_id = @linearIssueId`).run(values);
21
+ });
264
22
  }
265
23
  else {
266
- this.connection.prepare(`
267
- INSERT INTO issues (
268
- project_id, linear_issue_id, delegated_to_patchrelay, issue_class, issue_class_source, issue_triage_hash, issue_triage_result_json, parent_linear_issue_id, parent_issue_key, issue_key, title, description, url,
269
- priority, estimate,
270
- current_linear_state, current_linear_state_type, factory_state, pending_run_type, pending_run_context_json,
271
- branch_name, worktree_path, thread_id, active_run_id, status_comment_id,
272
- agent_session_id, last_linear_activity_key,
273
- pr_number, pr_url, pr_state, pr_is_draft, pr_head_sha, pr_author_login, pr_review_state, pr_check_status, last_blocking_review_head_sha,
274
- 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,
275
- 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,
276
- last_queue_signal_at, last_queue_incident_json,
277
- last_attempted_failure_head_sha, last_attempted_failure_signature, last_attempted_failure_at,
278
- last_published_patch_id, last_published_integration_tree_id, last_published_head_sha,
279
- parent_pr_branch,
280
- ci_repair_attempts, queue_repair_attempts, review_fix_attempts, zombie_recovery_attempts, last_zombie_recovery_at, orchestration_settle_until,
281
- updated_at
282
- ) VALUES (
283
- @projectId, @linearIssueId, @delegatedToPatchRelay, @issueClass, @issueClassSource, @issueTriageHash, @issueTriageResultJson, @parentLinearIssueId, @parentIssueKey, @issueKey, @title, @description, @url,
284
- @priority, @estimate,
285
- @currentLinearState, @currentLinearStateType, @factoryState, @pendingRunType, @pendingRunContextJson,
286
- @branchName, @worktreePath, @threadId, @activeRunId, @statusCommentId,
287
- @agentSessionId, @lastLinearActivityKey,
288
- @prNumber, @prUrl, @prState, @prIsDraft, @prHeadSha, @prAuthorLogin, @prReviewState, @prCheckStatus, @lastBlockingReviewHeadSha,
289
- @lastGitHubFailureSource, @lastGitHubFailureHeadSha, @lastGitHubFailureSignature, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureContextJson, @lastGitHubFailureAt,
290
- @lastGitHubCiSnapshotHeadSha, @lastGitHubCiSnapshotGateCheckName, @lastGitHubCiSnapshotGateCheckStatus, @lastGitHubCiSnapshotJson, @lastGitHubCiSnapshotSettledAt,
291
- @lastQueueSignalAt, @lastQueueIncidentJson,
292
- @lastAttemptedFailureHeadSha, @lastAttemptedFailureSignature, @lastAttemptedFailureAt,
293
- @lastPublishedPatchId, @lastPublishedIntegrationTreeId, @lastPublishedHeadSha,
294
- @parentPrBranch,
295
- @ciRepairAttempts, @queueRepairAttempts, @reviewFixAttempts, @zombieRecoveryAttempts, @lastZombieRecoveryAt, @orchestrationSettleUntil,
296
- @now
297
- )
298
- `).run({
24
+ const { columns, placeholders, values } = buildInsertBindings(params);
25
+ const sql = `INSERT INTO issues (project_id, linear_issue_id, ${columns.join(", ")}, updated_at) VALUES (@projectId, @linearIssueId, ${placeholders.join(", ")}, @now)`;
26
+ this.connection.prepare(sql).run({
27
+ ...values,
299
28
  projectId: params.projectId,
300
29
  linearIssueId: params.linearIssueId,
301
- delegatedToPatchRelay: params.delegatedToPatchRelay === false ? 0 : 1,
302
- issueClass: params.issueClass ?? null,
303
- issueClassSource: params.issueClassSource ?? null,
304
- issueTriageHash: params.issueTriageHash ?? null,
305
- issueTriageResultJson: params.issueTriageResultJson ?? null,
306
- parentLinearIssueId: params.parentLinearIssueId ?? null,
307
- parentIssueKey: params.parentIssueKey ?? null,
308
- issueKey: params.issueKey ?? null,
309
- title: params.title ?? null,
310
- description: params.description ?? null,
311
- url: params.url ?? null,
312
- priority: params.priority ?? null,
313
- estimate: params.estimate ?? null,
314
- currentLinearState: params.currentLinearState ?? null,
315
- currentLinearStateType: params.currentLinearStateType ?? null,
316
- factoryState: params.factoryState ?? "delegated",
317
- pendingRunType: params.pendingRunType ?? null,
318
- pendingRunContextJson: params.pendingRunContextJson ?? null,
319
- branchName: params.branchName ?? null,
320
- worktreePath: params.worktreePath ?? null,
321
- threadId: params.threadId ?? null,
322
- activeRunId: params.activeRunId ?? null,
323
- statusCommentId: params.statusCommentId ?? null,
324
- agentSessionId: params.agentSessionId ?? null,
325
- lastLinearActivityKey: params.lastLinearActivityKey ?? null,
326
- prNumber: params.prNumber ?? null,
327
- prUrl: params.prUrl ?? null,
328
- prState: params.prState ?? null,
329
- prIsDraft: params.prIsDraft == null ? null : params.prIsDraft ? 1 : 0,
330
- prHeadSha: params.prHeadSha ?? null,
331
- prAuthorLogin: params.prAuthorLogin ?? null,
332
- prReviewState: params.prReviewState ?? null,
333
- prCheckStatus: params.prCheckStatus ?? null,
334
- lastBlockingReviewHeadSha: params.lastBlockingReviewHeadSha ?? null,
335
- lastGitHubFailureSource: params.lastGitHubFailureSource ?? null,
336
- lastGitHubFailureHeadSha: params.lastGitHubFailureHeadSha ?? null,
337
- lastGitHubFailureSignature: params.lastGitHubFailureSignature ?? null,
338
- lastGitHubFailureCheckName: params.lastGitHubFailureCheckName ?? null,
339
- lastGitHubFailureCheckUrl: params.lastGitHubFailureCheckUrl ?? null,
340
- lastGitHubFailureContextJson: params.lastGitHubFailureContextJson ?? null,
341
- lastGitHubFailureAt: params.lastGitHubFailureAt ?? null,
342
- lastGitHubCiSnapshotHeadSha: params.lastGitHubCiSnapshotHeadSha ?? null,
343
- lastGitHubCiSnapshotGateCheckName: params.lastGitHubCiSnapshotGateCheckName ?? null,
344
- lastGitHubCiSnapshotGateCheckStatus: params.lastGitHubCiSnapshotGateCheckStatus ?? null,
345
- lastGitHubCiSnapshotJson: params.lastGitHubCiSnapshotJson ?? null,
346
- lastGitHubCiSnapshotSettledAt: params.lastGitHubCiSnapshotSettledAt ?? null,
347
- lastQueueSignalAt: params.lastQueueSignalAt ?? null,
348
- lastQueueIncidentJson: params.lastQueueIncidentJson ?? null,
349
- lastAttemptedFailureHeadSha: params.lastAttemptedFailureHeadSha ?? null,
350
- lastAttemptedFailureSignature: params.lastAttemptedFailureSignature ?? null,
351
- lastAttemptedFailureAt: params.lastAttemptedFailureAt ?? null,
352
- lastPublishedPatchId: params.lastPublishedPatchId ?? null,
353
- lastPublishedIntegrationTreeId: params.lastPublishedIntegrationTreeId ?? null,
354
- lastPublishedHeadSha: params.lastPublishedHeadSha ?? null,
355
- parentPrBranch: params.parentPrBranch ?? null,
356
- ciRepairAttempts: params.ciRepairAttempts ?? 0,
357
- queueRepairAttempts: params.queueRepairAttempts ?? 0,
358
- reviewFixAttempts: params.reviewFixAttempts ?? 0,
359
- zombieRecoveryAttempts: params.zombieRecoveryAttempts ?? 0,
360
- lastZombieRecoveryAt: params.lastZombieRecoveryAt ?? null,
361
- orchestrationSettleUntil: params.orchestrationSettleUntil ?? null,
362
30
  now,
363
31
  });
364
32
  }
@@ -416,6 +84,25 @@ export class IssueStore {
416
84
  .all();
417
85
  return rows.map(mapIssueRow);
418
86
  }
87
+ // Safety net for orphaned wakes: any delegated, non-terminal issue
88
+ // with at least one unprocessed session event but no active run.
89
+ // The orchestrator's enqueueIssue is the only path that drains these
90
+ // events, and a prior enqueueIssue call can be silently lost (worker
91
+ // race, lease contention, in-memory queue cleared by service restart).
92
+ // The idle reconciler iterates this set and re-enqueues each one.
93
+ listIdleIssuesWithPendingWake() {
94
+ const rows = this.connection
95
+ .prepare(`SELECT DISTINCT i.* FROM issues i
96
+ INNER JOIN issue_session_events e
97
+ ON e.project_id = i.project_id
98
+ AND e.linear_issue_id = i.linear_issue_id
99
+ WHERE e.processed_at IS NULL
100
+ AND i.active_run_id IS NULL
101
+ AND i.delegated_to_patchrelay = 1
102
+ AND i.factory_state NOT IN ('done', 'escalated', 'failed', 'awaiting_input')`)
103
+ .all();
104
+ return rows.map(mapIssueRow);
105
+ }
419
106
  listBlockedDelegatedIssues() {
420
107
  const rows = this.connection
421
108
  .prepare(`SELECT DISTINCT i.* FROM issues i
@@ -0,0 +1,119 @@
1
+ const booleanToInt = (value) => value === true || value === 1 ? 1 : 0;
2
+ const nullableBooleanToInt = (value) => {
3
+ if (value == null)
4
+ return null;
5
+ return value === true || value === 1 ? 1 : 0;
6
+ };
7
+ /**
8
+ * Source-of-truth column map for `upsertIssue`. Ordered to match the INSERT
9
+ * column list so a quick read shows the table shape end-to-end. Adding a new
10
+ * field is a one-line addition here instead of editing three parallel lists.
11
+ */
12
+ export const ISSUE_COLUMN_DEFS = {
13
+ delegatedToPatchRelay: { column: "delegated_to_patchrelay", transform: booleanToInt, insertDefault: 1 },
14
+ issueClass: { column: "issue_class" },
15
+ issueClassSource: { column: "issue_class_source" },
16
+ issueTriageHash: { column: "issue_triage_hash" },
17
+ issueTriageResultJson: { column: "issue_triage_result_json" },
18
+ parentLinearIssueId: { column: "parent_linear_issue_id" },
19
+ parentIssueKey: { column: "parent_issue_key" },
20
+ issueKey: { column: "issue_key", coalesce: true },
21
+ title: { column: "title", coalesce: true },
22
+ description: { column: "description", coalesce: true },
23
+ url: { column: "url", coalesce: true },
24
+ priority: { column: "priority" },
25
+ estimate: { column: "estimate" },
26
+ currentLinearState: { column: "current_linear_state", coalesce: true },
27
+ currentLinearStateType: { column: "current_linear_state_type", coalesce: true },
28
+ factoryState: { column: "factory_state", insertDefault: "delegated" },
29
+ pendingRunType: { column: "pending_run_type" },
30
+ pendingRunContextJson: { column: "pending_run_context_json" },
31
+ branchName: { column: "branch_name", coalesce: true },
32
+ worktreePath: { column: "worktree_path", coalesce: true },
33
+ threadId: { column: "thread_id" },
34
+ activeRunId: { column: "active_run_id" },
35
+ statusCommentId: { column: "status_comment_id" },
36
+ agentSessionId: { column: "agent_session_id" },
37
+ lastLinearActivityKey: { column: "last_linear_activity_key" },
38
+ prNumber: { column: "pr_number" },
39
+ prUrl: { column: "pr_url" },
40
+ prState: { column: "pr_state" },
41
+ prIsDraft: { column: "pr_is_draft", transform: nullableBooleanToInt },
42
+ prHeadSha: { column: "pr_head_sha" },
43
+ prAuthorLogin: { column: "pr_author_login" },
44
+ prReviewState: { column: "pr_review_state" },
45
+ prCheckStatus: { column: "pr_check_status" },
46
+ lastBlockingReviewHeadSha: { column: "last_blocking_review_head_sha" },
47
+ lastGitHubFailureSource: { column: "last_github_failure_source" },
48
+ lastGitHubFailureHeadSha: { column: "last_github_failure_head_sha" },
49
+ lastGitHubFailureSignature: { column: "last_github_failure_signature" },
50
+ lastGitHubFailureCheckName: { column: "last_github_failure_check_name" },
51
+ lastGitHubFailureCheckUrl: { column: "last_github_failure_check_url" },
52
+ lastGitHubFailureContextJson: { column: "last_github_failure_context_json" },
53
+ lastGitHubFailureAt: { column: "last_github_failure_at" },
54
+ lastGitHubCiSnapshotHeadSha: { column: "last_github_ci_snapshot_head_sha" },
55
+ lastGitHubCiSnapshotGateCheckName: { column: "last_github_ci_snapshot_gate_check_name" },
56
+ lastGitHubCiSnapshotGateCheckStatus: { column: "last_github_ci_snapshot_gate_check_status" },
57
+ lastGitHubCiSnapshotJson: { column: "last_github_ci_snapshot_json" },
58
+ lastGitHubCiSnapshotSettledAt: { column: "last_github_ci_snapshot_settled_at" },
59
+ lastQueueSignalAt: { column: "last_queue_signal_at" },
60
+ lastQueueIncidentJson: { column: "last_queue_incident_json" },
61
+ lastAttemptedFailureHeadSha: { column: "last_attempted_failure_head_sha" },
62
+ lastAttemptedFailureSignature: { column: "last_attempted_failure_signature" },
63
+ lastAttemptedFailureAt: { column: "last_attempted_failure_at" },
64
+ lastPublishedPatchId: { column: "last_published_patch_id" },
65
+ lastPublishedIntegrationTreeId: { column: "last_published_integration_tree_id" },
66
+ lastPublishedHeadSha: { column: "last_published_head_sha" },
67
+ parentPrBranch: { column: "parent_pr_branch" },
68
+ ciRepairAttempts: { column: "ci_repair_attempts", insertDefault: 0 },
69
+ queueRepairAttempts: { column: "queue_repair_attempts", insertDefault: 0 },
70
+ reviewFixAttempts: { column: "review_fix_attempts", insertDefault: 0 },
71
+ zombieRecoveryAttempts: { column: "zombie_recovery_attempts", insertDefault: 0 },
72
+ lastZombieRecoveryAt: { column: "last_zombie_recovery_at" },
73
+ orchestrationSettleUntil: { column: "orchestration_settle_until" },
74
+ };
75
+ export const ISSUE_COLUMN_KEYS = Object.keys(ISSUE_COLUMN_DEFS);
76
+ /**
77
+ * Builds the `SET col = @param` fragments and the bound-value bag for an
78
+ * UPDATE. Only fields explicitly set in `params` are included so we don't
79
+ * accidentally clobber other columns.
80
+ */
81
+ export function buildUpdateAssignments(params) {
82
+ const assignments = [];
83
+ const values = {};
84
+ for (const key of ISSUE_COLUMN_KEYS) {
85
+ const value = params[key];
86
+ if (value === undefined)
87
+ continue;
88
+ const def = ISSUE_COLUMN_DEFS[key];
89
+ const bound = def.transform ? def.transform(value) : value;
90
+ assignments.push(def.coalesce
91
+ ? `${def.column} = COALESCE(@${key}, ${def.column})`
92
+ : `${def.column} = @${key}`);
93
+ values[key] = bound;
94
+ }
95
+ return { assignments, values };
96
+ }
97
+ /**
98
+ * Builds the columns list, the placeholders list, and the bound-value bag for
99
+ * an INSERT. Every column is emitted (NULL if the param is omitted and no
100
+ * `insertDefault` is set) so the SQL stays stable across calls.
101
+ */
102
+ export function buildInsertBindings(params) {
103
+ const columns = [];
104
+ const placeholders = [];
105
+ const values = {};
106
+ for (const key of ISSUE_COLUMN_KEYS) {
107
+ const def = ISSUE_COLUMN_DEFS[key];
108
+ columns.push(def.column);
109
+ placeholders.push(`@${key}`);
110
+ const raw = params[key];
111
+ const resolved = raw !== undefined ? raw : def.insertDefault;
112
+ if (resolved === undefined) {
113
+ values[key] = null;
114
+ continue;
115
+ }
116
+ values[key] = def.transform ? def.transform(resolved) : resolved;
117
+ }
118
+ return { columns, placeholders, values };
119
+ }
package/dist/db.js CHANGED
@@ -182,6 +182,14 @@ export class PatchRelayDatabase {
182
182
  listIdleNonTerminalIssues() {
183
183
  return this.issues.listIdleNonTerminalIssues();
184
184
  }
185
+ /**
186
+ * Idle delegated issues that still have unprocessed session events.
187
+ * The idle reconciler re-enqueues these to recover from a silently
188
+ * dropped enqueueIssue (lease race, in-memory queue cleared at restart).
189
+ */
190
+ listIdleIssuesWithPendingWake() {
191
+ return this.issues.listIdleIssuesWithPendingWake();
192
+ }
185
193
  /**
186
194
  * Issues in delegated state with dependencies but no pending/active run.
187
195
  * Candidates for unblocking when their blockers complete.
@@ -1,12 +1,12 @@
1
1
  export class GitHubPrCommentHandler {
2
2
  db;
3
- enqueueIssue;
3
+ wakeDispatcher;
4
4
  logger;
5
5
  codex;
6
6
  feed;
7
- constructor(db, enqueueIssue, logger, codex, feed) {
7
+ constructor(db, wakeDispatcher, logger, codex, feed) {
8
8
  this.db = db;
9
- this.enqueueIssue = enqueueIssue;
9
+ this.wakeDispatcher = wakeDispatcher;
10
10
  this.logger = logger;
11
11
  this.codex = codex;
12
12
  this.feed = feed;
@@ -61,14 +61,9 @@ export class GitHubPrCommentHandler {
61
61
  }
62
62
  }
63
63
  }
64
- this.db.issueSessions.appendIssueSessionEvent({
65
- projectId: issue.projectId,
66
- linearIssueId: issue.linearIssueId,
64
+ this.wakeDispatcher.recordEventAndDispatch(issue.projectId, issue.linearIssueId, {
67
65
  eventType: "followup_comment",
68
66
  eventJson: JSON.stringify({ body, author }),
69
67
  });
70
- if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
71
- this.enqueueIssue(issue.projectId, issue.linearIssueId);
72
- }
73
68
  }
74
69
  }
@@ -10,11 +10,11 @@ import { maybeEnqueueGitHubReactiveRun } from "./github-webhook-reactive-run.js"
10
10
  import { maybeRunSequenceBackstop } from "./github-webhook-sequence-backstop.js";
11
11
  import { maybeFanChildRebaseWakes } from "./github-webhook-stack-coordination.js";
12
12
  import { handleGitHubTerminalPrEvent } from "./github-webhook-terminal-handler.js";
13
+ import { WakeDispatcher } from "./wake-dispatcher.js";
13
14
  export class GitHubWebhookHandler {
14
15
  config;
15
16
  db;
16
17
  linearProvider;
17
- enqueueIssue;
18
18
  logger;
19
19
  codex;
20
20
  feed;
@@ -22,18 +22,23 @@ export class GitHubWebhookHandler {
22
22
  ciSnapshotResolver;
23
23
  fetchImpl;
24
24
  prCommentHandler;
25
- constructor(config, db, linearProvider, enqueueIssue, logger, codex, feed, failureContextResolver = createGitHubFailureContextResolver(), ciSnapshotResolver = createGitHubCiSnapshotResolver(), fetchImpl = fetch) {
25
+ wakeDispatcher;
26
+ constructor(config, db, linearProvider, wakeDispatcherOrEnqueueIssue, logger, codex, feed, failureContextResolver = createGitHubFailureContextResolver(), ciSnapshotResolver = createGitHubCiSnapshotResolver(), fetchImpl = fetch) {
26
27
  this.config = config;
27
28
  this.db = db;
28
29
  this.linearProvider = linearProvider;
29
- this.enqueueIssue = enqueueIssue;
30
30
  this.logger = logger;
31
31
  this.codex = codex;
32
32
  this.feed = feed;
33
33
  this.failureContextResolver = failureContextResolver;
34
34
  this.ciSnapshotResolver = ciSnapshotResolver;
35
35
  this.fetchImpl = fetchImpl;
36
- this.prCommentHandler = new GitHubPrCommentHandler(db, enqueueIssue, logger, codex, feed);
36
+ // GitHub webhook handlers never release leases either — see
37
+ // WebhookHandler for the same rationale.
38
+ this.wakeDispatcher = wakeDispatcherOrEnqueueIssue instanceof WakeDispatcher
39
+ ? wakeDispatcherOrEnqueueIssue
40
+ : new WakeDispatcher(db, wakeDispatcherOrEnqueueIssue, () => undefined, logger, feed);
41
+ this.prCommentHandler = new GitHubPrCommentHandler(db, this.wakeDispatcher, logger, codex, feed);
37
42
  }
38
43
  async acceptGitHubWebhook(params) {
39
44
  if (this.db.webhookEvents.isWebhookDuplicate(params.deliveryId)) {
@@ -125,7 +130,7 @@ export class GitHubWebhookHandler {
125
130
  db: this.db,
126
131
  logger: this.logger,
127
132
  feed: this.feed,
128
- enqueueIssue: this.enqueueIssue,
133
+ wakeDispatcher: this.wakeDispatcher,
129
134
  issue: freshIssue,
130
135
  event,
131
136
  project,
@@ -152,7 +157,7 @@ export class GitHubWebhookHandler {
152
157
  db: this.db,
153
158
  logger: this.logger,
154
159
  ...(this.feed ? { feed: this.feed } : {}),
155
- enqueueIssue: this.enqueueIssue,
160
+ wakeDispatcher: this.wakeDispatcher,
156
161
  event,
157
162
  });
158
163
  }
@@ -161,7 +166,7 @@ export class GitHubWebhookHandler {
161
166
  config: this.config,
162
167
  db: this.db,
163
168
  linearProvider: this.linearProvider,
164
- enqueueIssue: this.enqueueIssue,
169
+ wakeDispatcher: this.wakeDispatcher,
165
170
  logger: this.logger,
166
171
  codex: this.codex,
167
172
  feed: this.feed,