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