patchrelay 0.35.10 → 0.35.12

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.
Files changed (50) hide show
  1. package/README.md +41 -9
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli/args.js +0 -1
  4. package/dist/cli/commands/issues.js +2 -56
  5. package/dist/cli/commands/watch.js +5 -0
  6. package/dist/cli/data.js +110 -47
  7. package/dist/cli/formatters/text.js +6 -90
  8. package/dist/cli/help.js +3 -8
  9. package/dist/cli/index.js +0 -48
  10. package/dist/cli/operator-client.js +0 -82
  11. package/dist/cli/watch/App.js +1 -12
  12. package/dist/cli/watch/HelpBar.js +2 -2
  13. package/dist/cli/watch/IssueDetailView.js +57 -26
  14. package/dist/cli/watch/IssueRow.js +71 -27
  15. package/dist/cli/watch/StatusBar.js +7 -4
  16. package/dist/cli/watch/state-visualization.js +48 -23
  17. package/dist/cli/watch/timeline-builder.js +2 -1
  18. package/dist/cli/watch/use-detail-stream.js +10 -104
  19. package/dist/cli/watch/use-watch-stream.js +11 -102
  20. package/dist/cli/watch/watch-state.js +18 -50
  21. package/dist/codex-thread-utils.js +3 -0
  22. package/dist/db/migrations.js +239 -2
  23. package/dist/db.js +628 -39
  24. package/dist/github-app-token.js +7 -0
  25. package/dist/github-failure-context.js +44 -1
  26. package/dist/github-rollup.js +47 -0
  27. package/dist/github-webhook-handler.js +248 -51
  28. package/dist/github-webhooks.js +5 -0
  29. package/dist/http.js +12 -264
  30. package/dist/idle-reconciliation.js +275 -74
  31. package/dist/issue-query-service.js +221 -129
  32. package/dist/issue-session-events.js +151 -0
  33. package/dist/issue-session.js +99 -0
  34. package/dist/linear-client.js +39 -25
  35. package/dist/linear-session-reporting.js +12 -0
  36. package/dist/linear-session-sync.js +253 -24
  37. package/dist/linear-workflow.js +33 -0
  38. package/dist/merge-queue-protocol.js +0 -51
  39. package/dist/preflight.js +1 -4
  40. package/dist/queue-health-monitor.js +11 -7
  41. package/dist/run-orchestrator.js +1295 -146
  42. package/dist/run-reporting.js +5 -3
  43. package/dist/service.js +279 -102
  44. package/dist/status-note.js +56 -0
  45. package/dist/waiting-reason.js +65 -0
  46. package/dist/webhook-handler.js +270 -79
  47. package/package.json +1 -1
  48. package/dist/cli/commands/feed.js +0 -60
  49. package/dist/cli/watch/FeedView.js +0 -28
  50. package/dist/cli/watch/use-feed-stream.js +0 -92
package/dist/db.js CHANGED
@@ -1,9 +1,90 @@
1
+ import { isIssueSessionReadyForExecution, deriveIssueSessionState, deriveIssueSessionReactiveIntent, deriveIssueSessionWakeReason, } from "./issue-session.js";
2
+ import { deriveSessionWakePlan, extractLatestAssistantSummary, } from "./issue-session-events.js";
1
3
  import { parseGitHubFailureContext } from "./github-failure-context.js";
4
+ import { deriveIssueStatusNote } from "./status-note.js";
2
5
  import { LinearInstallationStore } from "./db/linear-installation-store.js";
3
6
  import { OperatorFeedStore } from "./db/operator-feed-store.js";
4
7
  import { RepositoryLinkStore } from "./db/repository-link-store.js";
5
8
  import { runPatchRelayMigrations } from "./db/migrations.js";
6
9
  import { SqliteConnection, isoNow } from "./db/shared.js";
10
+ import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
11
+ function parseObjectJson(raw) {
12
+ if (!raw)
13
+ return undefined;
14
+ try {
15
+ const parsed = JSON.parse(raw);
16
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
17
+ ? parsed
18
+ : undefined;
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ }
24
+ function hasUnattemptedFailureSignature(issue, fallbackHeadSha) {
25
+ const signature = issue.lastGitHubFailureSignature;
26
+ if (!signature)
27
+ return false;
28
+ const headSha = issue.lastGitHubFailureHeadSha ?? fallbackHeadSha;
29
+ return issue.lastAttemptedFailureSignature !== signature
30
+ || (headSha !== undefined && issue.lastAttemptedFailureHeadSha !== headSha);
31
+ }
32
+ function deriveImplicitReactiveWake(issue) {
33
+ const reactiveIntent = deriveIssueSessionReactiveIntent({
34
+ activeRunId: issue.activeRunId,
35
+ prNumber: issue.prNumber,
36
+ prState: issue.prState,
37
+ prReviewState: issue.prReviewState,
38
+ prCheckStatus: issue.prCheckStatus,
39
+ latestFailureSource: issue.lastGitHubFailureSource,
40
+ });
41
+ if (!reactiveIntent)
42
+ return undefined;
43
+ if (reactiveIntent.runType === "ci_repair") {
44
+ const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
45
+ const snapshot = parseObjectJson(issue.lastGitHubCiSnapshotJson);
46
+ const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
47
+ ? failureContext.failureHeadSha
48
+ : issue.lastGitHubFailureHeadSha ?? issue.prHeadSha;
49
+ const failureSignature = issue.lastGitHubFailureSignature
50
+ ?? (fallbackHeadSha ? `implicit_branch_ci::${fallbackHeadSha}` : undefined);
51
+ if (!failureSignature || issue.prState !== "open")
52
+ return undefined;
53
+ if (issue.lastAttemptedFailureSignature === failureSignature
54
+ && (fallbackHeadSha === undefined || issue.lastAttemptedFailureHeadSha === fallbackHeadSha)) {
55
+ return undefined;
56
+ }
57
+ return {
58
+ runType: reactiveIntent.runType,
59
+ wakeReason: reactiveIntent.wakeReason,
60
+ context: {
61
+ ...failureContext,
62
+ failureSignature,
63
+ ...(fallbackHeadSha ? { failureHeadSha: fallbackHeadSha } : {}),
64
+ ...(issue.lastGitHubFailureCheckName ? { checkName: issue.lastGitHubFailureCheckName } : {}),
65
+ ...(snapshot ? { ciSnapshot: snapshot } : {}),
66
+ },
67
+ };
68
+ }
69
+ if (reactiveIntent.runType === "queue_repair") {
70
+ const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
71
+ const incidentContext = parseObjectJson(issue.lastQueueIncidentJson) ?? {};
72
+ const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
73
+ ? failureContext.failureHeadSha
74
+ : undefined;
75
+ if (!hasUnattemptedFailureSignature(issue, fallbackHeadSha))
76
+ return undefined;
77
+ return {
78
+ runType: reactiveIntent.runType,
79
+ wakeReason: reactiveIntent.wakeReason,
80
+ context: {
81
+ ...incidentContext,
82
+ ...failureContext,
83
+ },
84
+ };
85
+ }
86
+ return undefined;
87
+ }
7
88
  export class PatchRelayDatabase {
8
89
  connection;
9
90
  linearInstallations;
@@ -137,6 +218,10 @@ export class PatchRelayDatabase {
137
218
  sets.push("active_run_id = @activeRunId");
138
219
  values.activeRunId = params.activeRunId;
139
220
  }
221
+ if (params.statusCommentId !== undefined) {
222
+ sets.push("status_comment_id = @statusCommentId");
223
+ values.statusCommentId = params.statusCommentId;
224
+ }
140
225
  if (params.agentSessionId !== undefined) {
141
226
  sets.push("agent_session_id = @agentSessionId");
142
227
  values.agentSessionId = params.agentSessionId;
@@ -153,6 +238,14 @@ export class PatchRelayDatabase {
153
238
  sets.push("pr_state = @prState");
154
239
  values.prState = params.prState;
155
240
  }
241
+ if (params.prHeadSha !== undefined) {
242
+ sets.push("pr_head_sha = @prHeadSha");
243
+ values.prHeadSha = params.prHeadSha;
244
+ }
245
+ if (params.prAuthorLogin !== undefined) {
246
+ sets.push("pr_author_login = @prAuthorLogin");
247
+ values.prAuthorLogin = params.prAuthorLogin;
248
+ }
156
249
  if (params.prReviewState !== undefined) {
157
250
  sets.push("pr_review_state = @prReviewState");
158
251
  values.prReviewState = params.prReviewState;
@@ -245,10 +338,6 @@ export class PatchRelayDatabase {
245
338
  sets.push("last_zombie_recovery_at = @lastZombieRecoveryAt");
246
339
  values.lastZombieRecoveryAt = params.lastZombieRecoveryAt;
247
340
  }
248
- if (params.queueLabelApplied !== undefined) {
249
- sets.push("queue_label_applied = @queueLabelApplied");
250
- values.queueLabelApplied = params.queueLabelApplied ? 1 : 0;
251
- }
252
341
  this.connection.prepare(`UPDATE issues SET ${sets.join(", ")} WHERE project_id = @projectId AND linear_issue_id = @linearIssueId`).run(values);
253
342
  }
254
343
  else {
@@ -257,9 +346,9 @@ export class PatchRelayDatabase {
257
346
  project_id, linear_issue_id, issue_key, title, description, url,
258
347
  priority, estimate,
259
348
  current_linear_state, current_linear_state_type, factory_state, pending_run_type, pending_run_context_json,
260
- branch_name, worktree_path, thread_id, active_run_id,
349
+ branch_name, worktree_path, thread_id, active_run_id, status_comment_id,
261
350
  agent_session_id,
262
- pr_number, pr_url, pr_state, pr_review_state, pr_check_status,
351
+ pr_number, pr_url, pr_state, pr_head_sha, pr_author_login, pr_review_state, pr_check_status,
263
352
  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,
264
353
  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,
265
354
  last_queue_signal_at, last_queue_incident_json,
@@ -269,9 +358,9 @@ export class PatchRelayDatabase {
269
358
  @projectId, @linearIssueId, @issueKey, @title, @description, @url,
270
359
  @priority, @estimate,
271
360
  @currentLinearState, @currentLinearStateType, @factoryState, @pendingRunType, @pendingRunContextJson,
272
- @branchName, @worktreePath, @threadId, @activeRunId,
361
+ @branchName, @worktreePath, @threadId, @activeRunId, @statusCommentId,
273
362
  @agentSessionId,
274
- @prNumber, @prUrl, @prState, @prReviewState, @prCheckStatus,
363
+ @prNumber, @prUrl, @prState, @prHeadSha, @prAuthorLogin, @prReviewState, @prCheckStatus,
275
364
  @lastGitHubFailureSource, @lastGitHubFailureHeadSha, @lastGitHubFailureSignature, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureContextJson, @lastGitHubFailureAt,
276
365
  @lastGitHubCiSnapshotHeadSha, @lastGitHubCiSnapshotGateCheckName, @lastGitHubCiSnapshotGateCheckStatus, @lastGitHubCiSnapshotJson, @lastGitHubCiSnapshotSettledAt,
277
366
  @lastQueueSignalAt, @lastQueueIncidentJson,
@@ -296,10 +385,13 @@ export class PatchRelayDatabase {
296
385
  worktreePath: params.worktreePath ?? null,
297
386
  threadId: params.threadId ?? null,
298
387
  activeRunId: params.activeRunId ?? null,
388
+ statusCommentId: params.statusCommentId ?? null,
299
389
  agentSessionId: params.agentSessionId ?? null,
300
390
  prNumber: params.prNumber ?? null,
301
391
  prUrl: params.prUrl ?? null,
302
392
  prState: params.prState ?? null,
393
+ prHeadSha: params.prHeadSha ?? null,
394
+ prAuthorLogin: params.prAuthorLogin ?? null,
303
395
  prReviewState: params.prReviewState ?? null,
304
396
  prCheckStatus: params.prCheckStatus ?? null,
305
397
  lastGitHubFailureSource: params.lastGitHubFailureSource ?? null,
@@ -321,7 +413,9 @@ export class PatchRelayDatabase {
321
413
  now,
322
414
  });
323
415
  }
324
- return this.getIssue(params.projectId, params.linearIssueId);
416
+ const updated = this.getIssue(params.projectId, params.linearIssueId);
417
+ this.syncIssueSessionFromIssue(updated);
418
+ return updated;
325
419
  }
326
420
  getIssue(projectId, linearIssueId) {
327
421
  const row = this.connection
@@ -345,6 +439,254 @@ export class PatchRelayDatabase {
345
439
  const row = this.connection.prepare("SELECT * FROM issues WHERE pr_number = ?").get(prNumber);
346
440
  return row ? mapIssueRow(row) : undefined;
347
441
  }
442
+ getIssueSession(projectId, linearIssueId) {
443
+ const row = this.connection
444
+ .prepare("SELECT * FROM issue_sessions WHERE project_id = ? AND linear_issue_id = ?")
445
+ .get(projectId, linearIssueId);
446
+ return row ? mapIssueSessionRow(row) : undefined;
447
+ }
448
+ getIssueSessionByKey(issueKey) {
449
+ const row = this.connection.prepare("SELECT * FROM issue_sessions WHERE issue_key = ?").get(issueKey);
450
+ return row ? mapIssueSessionRow(row) : undefined;
451
+ }
452
+ appendIssueSessionEvent(params) {
453
+ if (params.dedupeKey) {
454
+ const existing = this.connection.prepare(`
455
+ SELECT * FROM issue_session_events
456
+ WHERE project_id = ? AND linear_issue_id = ? AND dedupe_key = ? AND processed_at IS NULL
457
+ ORDER BY id DESC LIMIT 1
458
+ `).get(params.projectId, params.linearIssueId, params.dedupeKey);
459
+ if (existing)
460
+ return mapIssueSessionEventRow(existing);
461
+ }
462
+ const now = isoNow();
463
+ const result = this.connection.prepare(`
464
+ INSERT INTO issue_session_events (
465
+ project_id, linear_issue_id, event_type, event_json, dedupe_key, created_at
466
+ ) VALUES (?, ?, ?, ?, ?, ?)
467
+ `).run(params.projectId, params.linearIssueId, params.eventType, params.eventJson ?? null, params.dedupeKey ?? null, now);
468
+ return this.getIssueSessionEvent(Number(result.lastInsertRowid));
469
+ }
470
+ appendIssueSessionEventWithLease(lease, params) {
471
+ return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => this.appendIssueSessionEvent(params));
472
+ }
473
+ appendIssueSessionEventRespectingActiveLease(projectId, linearIssueId, params) {
474
+ const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
475
+ if (!lease) {
476
+ return this.appendIssueSessionEvent(params);
477
+ }
478
+ return this.appendIssueSessionEventWithLease(lease, params);
479
+ }
480
+ getIssueSessionEvent(id) {
481
+ const row = this.connection.prepare("SELECT * FROM issue_session_events WHERE id = ?").get(id);
482
+ return row ? mapIssueSessionEventRow(row) : undefined;
483
+ }
484
+ listIssueSessionEvents(projectId, linearIssueId, options) {
485
+ const conditions = ["project_id = ?", "linear_issue_id = ?"];
486
+ const values = [projectId, linearIssueId];
487
+ if (options?.pendingOnly) {
488
+ conditions.push("processed_at IS NULL");
489
+ }
490
+ let query = `SELECT * FROM issue_session_events WHERE ${conditions.join(" AND ")} ORDER BY id`;
491
+ if (options?.limit !== undefined) {
492
+ query += " LIMIT ?";
493
+ values.push(options.limit);
494
+ }
495
+ const rows = this.connection.prepare(query).all(...values);
496
+ return rows.map(mapIssueSessionEventRow);
497
+ }
498
+ consumeIssueSessionEvents(projectId, linearIssueId, eventIds, runId) {
499
+ if (eventIds.length === 0)
500
+ return;
501
+ const now = isoNow();
502
+ const placeholders = eventIds.map(() => "?").join(", ");
503
+ this.connection.prepare(`
504
+ UPDATE issue_session_events
505
+ SET processed_at = ?, consumed_by_run_id = ?
506
+ WHERE project_id = ? AND linear_issue_id = ? AND id IN (${placeholders}) AND processed_at IS NULL
507
+ `).run(now, runId, projectId, linearIssueId, ...eventIds);
508
+ }
509
+ clearPendingIssueSessionEvents(projectId, linearIssueId) {
510
+ this.connection.prepare(`
511
+ UPDATE issue_session_events
512
+ SET processed_at = ?, consumed_by_run_id = NULL
513
+ WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
514
+ `).run(isoNow(), projectId, linearIssueId);
515
+ }
516
+ hasPendingIssueSessionEvents(projectId, linearIssueId) {
517
+ const row = this.connection.prepare(`
518
+ SELECT 1
519
+ FROM issue_session_events
520
+ WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
521
+ LIMIT 1
522
+ `).get(projectId, linearIssueId);
523
+ return row !== undefined;
524
+ }
525
+ peekIssueSessionWake(projectId, linearIssueId) {
526
+ const issue = this.getIssue(projectId, linearIssueId);
527
+ if (!issue)
528
+ return undefined;
529
+ const events = this.listIssueSessionEvents(projectId, linearIssueId, { pendingOnly: true });
530
+ const plan = deriveSessionWakePlan(issue, events);
531
+ if (plan?.runType) {
532
+ return {
533
+ eventIds: events.map((event) => event.id),
534
+ runType: plan.runType,
535
+ context: plan.context,
536
+ ...(plan.wakeReason ? { wakeReason: plan.wakeReason } : {}),
537
+ resumeThread: plan.resumeThread,
538
+ };
539
+ }
540
+ const implicitWake = deriveImplicitReactiveWake(issue);
541
+ if (!implicitWake)
542
+ return undefined;
543
+ return {
544
+ eventIds: [],
545
+ runType: implicitWake.runType,
546
+ context: implicitWake.context,
547
+ wakeReason: implicitWake.wakeReason,
548
+ resumeThread: false,
549
+ };
550
+ }
551
+ acquireIssueSessionLease(params) {
552
+ const now = params.now ?? isoNow();
553
+ const result = this.connection.prepare(`
554
+ UPDATE issue_sessions
555
+ SET lease_id = ?, worker_id = ?, leased_until = ?, updated_at = ?
556
+ WHERE project_id = ? AND linear_issue_id = ?
557
+ AND (leased_until IS NULL OR leased_until <= ? OR lease_id = ?)
558
+ `).run(params.leaseId, params.workerId, params.leasedUntil, now, params.projectId, params.linearIssueId, now, params.leaseId);
559
+ return Number(result.changes ?? 0) > 0;
560
+ }
561
+ forceAcquireIssueSessionLease(params) {
562
+ const now = params.now ?? isoNow();
563
+ const result = this.connection.prepare(`
564
+ UPDATE issue_sessions
565
+ SET lease_id = ?, worker_id = ?, leased_until = ?, updated_at = ?
566
+ WHERE project_id = ? AND linear_issue_id = ?
567
+ `).run(params.leaseId, params.workerId, params.leasedUntil, now, params.projectId, params.linearIssueId);
568
+ return Number(result.changes ?? 0) > 0;
569
+ }
570
+ renewIssueSessionLease(params) {
571
+ const now = params.now ?? isoNow();
572
+ const result = this.connection.prepare(`
573
+ UPDATE issue_sessions
574
+ SET leased_until = ?, updated_at = ?
575
+ WHERE project_id = ? AND linear_issue_id = ? AND lease_id = ?
576
+ `).run(params.leasedUntil, now, params.projectId, params.linearIssueId, params.leaseId);
577
+ return Number(result.changes ?? 0) > 0;
578
+ }
579
+ releaseIssueSessionLease(projectId, linearIssueId, leaseId) {
580
+ this.connection.prepare(`
581
+ UPDATE issue_sessions
582
+ SET lease_id = NULL, worker_id = NULL, leased_until = NULL, updated_at = ?
583
+ WHERE project_id = ? AND linear_issue_id = ? AND (? IS NULL OR lease_id = ?)
584
+ `).run(isoNow(), projectId, linearIssueId, leaseId ?? null, leaseId ?? null);
585
+ }
586
+ releaseExpiredIssueSessionLeases(now = isoNow()) {
587
+ this.connection.prepare(`
588
+ UPDATE issue_sessions
589
+ SET lease_id = NULL, worker_id = NULL, leased_until = NULL, updated_at = ?
590
+ WHERE leased_until IS NOT NULL AND leased_until <= ?
591
+ `).run(now, now);
592
+ }
593
+ hasActiveIssueSessionLease(projectId, linearIssueId, leaseId, now = isoNow()) {
594
+ const row = this.connection.prepare(`
595
+ SELECT 1
596
+ FROM issue_sessions
597
+ WHERE project_id = ? AND linear_issue_id = ? AND lease_id = ?
598
+ AND leased_until IS NOT NULL
599
+ AND leased_until > ?
600
+ LIMIT 1
601
+ `).get(projectId, linearIssueId, leaseId, now);
602
+ return row !== undefined;
603
+ }
604
+ getActiveIssueSessionLease(projectId, linearIssueId, now = isoNow()) {
605
+ const row = this.connection.prepare(`
606
+ SELECT lease_id
607
+ FROM issue_sessions
608
+ WHERE project_id = ? AND linear_issue_id = ?
609
+ AND lease_id IS NOT NULL
610
+ AND leased_until IS NOT NULL
611
+ AND leased_until > ?
612
+ LIMIT 1
613
+ `).get(projectId, linearIssueId, now);
614
+ const leaseId = typeof row?.lease_id === "string" ? row.lease_id : undefined;
615
+ if (!leaseId)
616
+ return undefined;
617
+ return { projectId, linearIssueId, leaseId };
618
+ }
619
+ withIssueSessionLease(projectId, linearIssueId, leaseId, fn) {
620
+ return this.transaction(() => {
621
+ if (!this.hasActiveIssueSessionLease(projectId, linearIssueId, leaseId)) {
622
+ return undefined;
623
+ }
624
+ return fn();
625
+ });
626
+ }
627
+ upsertIssueWithLease(lease, params) {
628
+ return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => this.upsertIssue(params));
629
+ }
630
+ upsertIssueRespectingActiveLease(projectId, linearIssueId, params) {
631
+ const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
632
+ if (!lease) {
633
+ return this.upsertIssue(params);
634
+ }
635
+ return this.upsertIssueWithLease(lease, params);
636
+ }
637
+ finishRunWithLease(lease, runId, params) {
638
+ return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
639
+ this.finishRun(runId, params);
640
+ return true;
641
+ }) ?? false;
642
+ }
643
+ finishRunRespectingActiveLease(projectId, linearIssueId, runId, params) {
644
+ const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
645
+ if (!lease) {
646
+ this.finishRun(runId, params);
647
+ return true;
648
+ }
649
+ return this.finishRunWithLease(lease, runId, params);
650
+ }
651
+ updateRunThreadWithLease(lease, runId, params) {
652
+ return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
653
+ this.updateRunThread(runId, params);
654
+ return true;
655
+ }) ?? false;
656
+ }
657
+ consumeIssueSessionEventsWithLease(lease, eventIds, runId) {
658
+ return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
659
+ this.consumeIssueSessionEvents(lease.projectId, lease.linearIssueId, eventIds, runId);
660
+ return true;
661
+ }) ?? false;
662
+ }
663
+ clearPendingIssueSessionEventsWithLease(lease) {
664
+ return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
665
+ this.clearPendingIssueSessionEvents(lease.projectId, lease.linearIssueId);
666
+ return true;
667
+ }) ?? false;
668
+ }
669
+ clearPendingIssueSessionEventsRespectingActiveLease(projectId, linearIssueId) {
670
+ const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
671
+ if (!lease) {
672
+ this.clearPendingIssueSessionEvents(projectId, linearIssueId);
673
+ return true;
674
+ }
675
+ return this.clearPendingIssueSessionEventsWithLease(lease);
676
+ }
677
+ setIssueSessionLastWakeReasonWithLease(lease, lastWakeReason) {
678
+ return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
679
+ this.setIssueSessionLastWakeReason(lease.projectId, lease.linearIssueId, lastWakeReason);
680
+ return true;
681
+ }) ?? false;
682
+ }
683
+ setIssueSessionLastWakeReason(projectId, linearIssueId, lastWakeReason) {
684
+ this.connection.prepare(`
685
+ UPDATE issue_sessions
686
+ SET last_wake_reason = ?, updated_at = ?
687
+ WHERE project_id = ? AND linear_issue_id = ?
688
+ `).run(lastWakeReason ?? null, isoNow(), projectId, linearIssueId);
689
+ }
348
690
  setBranchOwner(projectId, linearIssueId, owner) {
349
691
  this.connection.prepare(`
350
692
  UPDATE issues
@@ -352,6 +694,24 @@ export class PatchRelayDatabase {
352
694
  WHERE project_id = ? AND linear_issue_id = ?
353
695
  `).run(owner, isoNow(), isoNow(), projectId, linearIssueId);
354
696
  }
697
+ setBranchOwnerWithLease(lease, owner) {
698
+ return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
699
+ this.setBranchOwner(lease.projectId, lease.linearIssueId, owner);
700
+ return true;
701
+ }) ?? false;
702
+ }
703
+ setBranchOwnerRespectingActiveLease(projectId, linearIssueId, owner) {
704
+ const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
705
+ if (!lease) {
706
+ this.setBranchOwner(projectId, linearIssueId, owner);
707
+ return true;
708
+ }
709
+ return this.setBranchOwnerWithLease(lease, owner);
710
+ }
711
+ releaseIssueSessionLeaseRespectingActiveLease(projectId, linearIssueId) {
712
+ const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
713
+ this.releaseIssueSessionLease(projectId, linearIssueId, lease?.leaseId);
714
+ }
355
715
  replaceIssueDependencies(params) {
356
716
  const now = isoNow();
357
717
  this.connection
@@ -448,30 +808,13 @@ export class PatchRelayDatabase {
448
808
  return Number(row?.count ?? 0);
449
809
  }
450
810
  listIssuesReadyForExecution() {
451
- const rows = this.connection
452
- .prepare(`
453
- SELECT i.project_id, i.linear_issue_id
454
- FROM issues i
455
- WHERE i.pending_run_type IS NOT NULL
456
- AND i.active_run_id IS NULL
457
- AND NOT EXISTS (
458
- SELECT 1
459
- FROM issue_dependencies d
460
- LEFT JOIN issues blockers
461
- ON blockers.project_id = d.project_id
462
- AND blockers.linear_issue_id = d.blocker_linear_issue_id
463
- WHERE d.project_id = i.project_id
464
- AND d.linear_issue_id = i.linear_issue_id
465
- AND (
466
- COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
467
- AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
468
- )
469
- )
470
- `)
471
- .all();
472
- return rows.map((row) => ({
473
- projectId: String(row.project_id),
474
- linearIssueId: String(row.linear_issue_id),
811
+ return this.listIssues()
812
+ .filter((issue) => issue.activeRunId === undefined)
813
+ .filter((issue) => this.countUnresolvedBlockers(issue.projectId, issue.linearIssueId) === 0)
814
+ .filter((issue) => issue.pendingRunType !== undefined || this.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined)
815
+ .map((issue) => ({
816
+ projectId: issue.projectId,
817
+ linearIssueId: issue.linearIssueId,
475
818
  }));
476
819
  }
477
820
  /**
@@ -529,7 +872,12 @@ export class PatchRelayDatabase {
529
872
  INSERT INTO runs (issue_id, project_id, linear_issue_id, run_type, status, prompt_text, started_at)
530
873
  VALUES (?, ?, ?, ?, 'queued', ?, ?)
531
874
  `).run(params.issueId, params.projectId, params.linearIssueId, params.runType, params.promptText ?? null, now);
532
- return this.getRun(Number(result.lastInsertRowid));
875
+ const run = this.getRun(Number(result.lastInsertRowid));
876
+ const issue = this.getIssue(params.projectId, params.linearIssueId);
877
+ if (issue) {
878
+ this.syncIssueSessionFromIssue(issue, { lastRunType: run.runType });
879
+ }
880
+ return run;
533
881
  }
534
882
  getRun(id) {
535
883
  const row = this.connection.prepare("SELECT * FROM runs WHERE id = ?").get(id);
@@ -571,7 +919,16 @@ export class PatchRelayDatabase {
571
919
  turn_id = COALESCE(?, turn_id),
572
920
  status = 'running'
573
921
  WHERE id = ?
922
+ AND ended_at IS NULL
923
+ AND status IN ('queued', 'running')
574
924
  `).run(params.threadId, params.parentThreadId ?? null, params.turnId ?? null, runId);
925
+ const run = this.getRun(runId);
926
+ if (!run)
927
+ return;
928
+ const issue = this.getIssue(run.projectId, run.linearIssueId);
929
+ if (issue) {
930
+ this.syncIssueSessionFromIssue(issue);
931
+ }
575
932
  }
576
933
  updateRunTurnId(runId, turnId) {
577
934
  this.connection.prepare("UPDATE runs SET turn_id = ? WHERE id = ?").run(turnId, runId);
@@ -589,6 +946,16 @@ export class PatchRelayDatabase {
589
946
  ended_at = ?
590
947
  WHERE id = ?
591
948
  `).run(params.status, params.threadId ?? null, params.turnId ?? null, params.failureReason ?? null, params.summaryJson ?? null, params.reportJson ?? null, now, runId);
949
+ const run = this.getRun(runId);
950
+ if (!run)
951
+ return;
952
+ const issue = this.getIssue(run.projectId, run.linearIssueId);
953
+ if (issue) {
954
+ this.syncIssueSessionFromIssue(issue, {
955
+ summaryText: extractLatestAssistantSummary(this.getRun(runId) ?? run),
956
+ lastRunType: run.runType,
957
+ });
958
+ }
592
959
  }
593
960
  // ─── Thread Events (kept for extended history) ────────────────────
594
961
  saveThreadEvent(params) {
@@ -613,9 +980,33 @@ export class PatchRelayDatabase {
613
980
  }
614
981
  // ─── View builders ──────────────────────────────────────────────
615
982
  issueToTrackedIssue(issue) {
983
+ const session = this.getIssueSession(issue.projectId, issue.linearIssueId);
616
984
  const blockedBy = this.listIssueDependencies(issue.projectId, issue.linearIssueId);
617
985
  const unresolvedBlockedBy = blockedBy.filter((entry) => !isResolvedLinearState(entry.blockerCurrentLinearStateType, entry.blockerCurrentLinearState));
986
+ const pendingWake = this.peekIssueSessionWake(issue.projectId, issue.linearIssueId);
618
987
  const failureContext = parseGitHubFailureContext(issue.lastGitHubFailureContextJson);
988
+ const blockedByKeys = unresolvedBlockedBy.map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId);
989
+ const waitingReason = derivePatchRelayWaitingReason({
990
+ ...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
991
+ blockedByKeys,
992
+ factoryState: issue.factoryState,
993
+ pendingRunType: issue.pendingRunType,
994
+ prNumber: issue.prNumber,
995
+ prReviewState: issue.prReviewState,
996
+ prCheckStatus: issue.prCheckStatus,
997
+ latestFailureCheckName: issue.lastGitHubFailureCheckName,
998
+ });
999
+ const latestRun = this.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
1000
+ const latestEvent = this.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1);
1001
+ const statusNote = deriveIssueStatusNote({
1002
+ issue,
1003
+ sessionSummary: session?.summaryText,
1004
+ latestRun,
1005
+ latestEvent,
1006
+ failureSummary: failureContext?.summary,
1007
+ blockedByKeys,
1008
+ waitingReason,
1009
+ });
619
1010
  return {
620
1011
  id: issue.id,
621
1012
  projectId: issue.projectId,
@@ -623,17 +1014,31 @@ export class PatchRelayDatabase {
623
1014
  ...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
624
1015
  ...(issue.title ? { title: issue.title } : {}),
625
1016
  ...(issue.url ? { issueUrl: issue.url } : {}),
1017
+ ...(statusNote ? { statusNote } : {}),
626
1018
  ...(issue.currentLinearState ? { currentLinearState: issue.currentLinearState } : {}),
1019
+ ...(session?.sessionState ? { sessionState: session.sessionState } : {}),
627
1020
  factoryState: issue.factoryState,
628
1021
  blockedByCount: unresolvedBlockedBy.length,
629
- blockedByKeys: unresolvedBlockedBy
630
- .map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId),
631
- readyForExecution: issue.pendingRunType !== undefined && issue.activeRunId === undefined && unresolvedBlockedBy.length === 0,
1022
+ blockedByKeys,
1023
+ readyForExecution: isIssueSessionReadyForExecution({
1024
+ sessionState: session?.sessionState,
1025
+ factoryState: issue.factoryState,
1026
+ activeRunId: issue.activeRunId,
1027
+ blockedByCount: unresolvedBlockedBy.length,
1028
+ hasPendingWake: pendingWake !== undefined,
1029
+ hasLegacyPendingRun: issue.pendingRunType !== undefined,
1030
+ ...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
1031
+ ...(issue.prState ? { prState: issue.prState } : {}),
1032
+ ...(issue.prReviewState ? { prReviewState: issue.prReviewState } : {}),
1033
+ ...(issue.prCheckStatus ? { prCheckStatus: issue.prCheckStatus } : {}),
1034
+ ...(issue.lastGitHubFailureSource ? { latestFailureSource: issue.lastGitHubFailureSource } : {}),
1035
+ }),
632
1036
  ...(issue.lastGitHubFailureSource ? { latestFailureSource: issue.lastGitHubFailureSource } : {}),
633
1037
  ...(issue.lastGitHubFailureHeadSha ? { latestFailureHeadSha: issue.lastGitHubFailureHeadSha } : {}),
634
1038
  ...(issue.lastGitHubFailureCheckName ? { latestFailureCheckName: issue.lastGitHubFailureCheckName } : {}),
635
1039
  ...(failureContext?.stepName ? { latestFailureStepName: failureContext.stepName } : {}),
636
1040
  ...(failureContext?.summary ? { latestFailureSummary: failureContext.summary } : {}),
1041
+ ...(waitingReason ? { waitingReason } : {}),
637
1042
  ...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
638
1043
  ...(issue.agentSessionId ? { activeAgentSessionId: issue.agentSessionId } : {}),
639
1044
  updatedAt: issue.updatedAt,
@@ -647,6 +1052,44 @@ export class PatchRelayDatabase {
647
1052
  const issue = this.getIssueByKey(issueKey);
648
1053
  return issue ? this.issueToTrackedIssue(issue) : undefined;
649
1054
  }
1055
+ listIssues() {
1056
+ const rows = this.connection
1057
+ .prepare("SELECT * FROM issues ORDER BY updated_at DESC")
1058
+ .all();
1059
+ return rows.map(mapIssueRow);
1060
+ }
1061
+ listIssuesWithAgentSessions() {
1062
+ const rows = this.connection
1063
+ .prepare("SELECT * FROM issues WHERE agent_session_id IS NOT NULL ORDER BY updated_at DESC")
1064
+ .all();
1065
+ return rows.map(mapIssueRow);
1066
+ }
1067
+ findLatestAgentSessionIdForIssue(linearIssueId) {
1068
+ const row = this.connection.prepare(`
1069
+ SELECT COALESCE(
1070
+ json_extract(payload_json, '$.agentSession.id'),
1071
+ json_extract(payload_json, '$.data.agentSession.id'),
1072
+ json_extract(payload_json, '$.agentSessionId'),
1073
+ json_extract(payload_json, '$.data.agentSessionId')
1074
+ ) AS agent_session_id
1075
+ FROM webhook_events
1076
+ WHERE COALESCE(
1077
+ json_extract(payload_json, '$.agentSession.issueId'),
1078
+ json_extract(payload_json, '$.data.agentSession.issueId'),
1079
+ json_extract(payload_json, '$.agentSession.issue.id'),
1080
+ json_extract(payload_json, '$.data.agentSession.issue.id')
1081
+ ) = ?
1082
+ AND COALESCE(
1083
+ json_extract(payload_json, '$.agentSession.id'),
1084
+ json_extract(payload_json, '$.data.agentSession.id'),
1085
+ json_extract(payload_json, '$.agentSessionId'),
1086
+ json_extract(payload_json, '$.data.agentSessionId')
1087
+ ) IS NOT NULL
1088
+ ORDER BY id DESC
1089
+ LIMIT 1
1090
+ `).get(linearIssueId);
1091
+ return row?.agent_session_id != null ? String(row.agent_session_id) : undefined;
1092
+ }
650
1093
  // ─── Issue overview for query service ─────────────────────────────
651
1094
  getIssueOverview(issueKey) {
652
1095
  const issue = this.getIssueByKey(issueKey);
@@ -659,6 +1102,105 @@ export class PatchRelayDatabase {
659
1102
  ...(activeRun ? { activeRun } : {}),
660
1103
  };
661
1104
  }
1105
+ syncIssueSessionFromIssue(issue, options) {
1106
+ const tracked = this.issueToTrackedIssue(issue);
1107
+ const existing = this.getIssueSession(issue.projectId, issue.linearIssueId);
1108
+ const latestRun = this.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
1109
+ const latestRunType = options?.lastRunType ?? latestRun?.runType ?? existing?.lastRunType;
1110
+ const summaryText = this.resolveIssueSessionSummary(issue, latestRun, existing?.summaryText, options?.summaryText);
1111
+ const activeThreadId = issue.threadId ?? existing?.activeThreadId;
1112
+ const threadGeneration = activeThreadId && activeThreadId !== existing?.activeThreadId
1113
+ ? (existing?.threadGeneration ?? 0) + 1
1114
+ : (existing?.threadGeneration ?? (activeThreadId ? 1 : 0));
1115
+ const sessionState = deriveIssueSessionState({
1116
+ ...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
1117
+ factoryState: issue.factoryState,
1118
+ });
1119
+ const lastWakeReason = options?.lastWakeReason
1120
+ ?? deriveIssueSessionWakeReason({
1121
+ pendingRunType: issue.pendingRunType,
1122
+ factoryState: issue.factoryState,
1123
+ prNumber: issue.prNumber,
1124
+ prState: issue.prState,
1125
+ prReviewState: issue.prReviewState,
1126
+ prCheckStatus: issue.prCheckStatus,
1127
+ latestFailureSource: issue.lastGitHubFailureSource,
1128
+ })
1129
+ ?? existing?.lastWakeReason;
1130
+ const now = isoNow();
1131
+ if (existing) {
1132
+ this.connection.prepare(`
1133
+ UPDATE issue_sessions SET
1134
+ issue_key = ?,
1135
+ repo_id = ?,
1136
+ branch_name = ?,
1137
+ worktree_path = ?,
1138
+ pr_number = ?,
1139
+ pr_head_sha = ?,
1140
+ pr_author_login = ?,
1141
+ session_state = ?,
1142
+ waiting_reason = ?,
1143
+ summary_text = ?,
1144
+ active_thread_id = ?,
1145
+ thread_generation = ?,
1146
+ active_run_id = ?,
1147
+ last_run_type = ?,
1148
+ last_wake_reason = ?,
1149
+ ci_repair_attempts = ?,
1150
+ queue_repair_attempts = ?,
1151
+ review_fix_attempts = ?,
1152
+ updated_at = ?
1153
+ WHERE project_id = ? AND linear_issue_id = ?
1154
+ `).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);
1155
+ return;
1156
+ }
1157
+ this.connection.prepare(`
1158
+ INSERT INTO issue_sessions (
1159
+ project_id, linear_issue_id, issue_key, repo_id, branch_name, worktree_path,
1160
+ pr_number, pr_head_sha, pr_author_login, session_state, waiting_reason, summary_text,
1161
+ active_thread_id, thread_generation, active_run_id, last_run_type, last_wake_reason,
1162
+ ci_repair_attempts, queue_repair_attempts, review_fix_attempts,
1163
+ created_at, updated_at
1164
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1165
+ `).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);
1166
+ }
1167
+ resolveIssueSessionSummary(issue, latestRun, existingSummaryText, explicitSummaryText) {
1168
+ if (explicitSummaryText?.trim()) {
1169
+ return explicitSummaryText;
1170
+ }
1171
+ const latestSummary = extractLatestAssistantSummary(latestRun);
1172
+ if (this.shouldKeepPreviousIssueSummary(issue, latestRun)) {
1173
+ return this.findLatestCompletedRunSummary(issue.projectId, issue.linearIssueId)
1174
+ ?? existingSummaryText
1175
+ ?? latestSummary;
1176
+ }
1177
+ return latestSummary ?? existingSummaryText;
1178
+ }
1179
+ shouldKeepPreviousIssueSummary(issue, latestRun) {
1180
+ if (!latestRun || latestRun.status !== "failed") {
1181
+ return false;
1182
+ }
1183
+ if (latestRun.summaryJson || latestRun.reportJson) {
1184
+ return false;
1185
+ }
1186
+ return issue.factoryState === "pr_open"
1187
+ || issue.factoryState === "awaiting_queue"
1188
+ || issue.factoryState === "done";
1189
+ }
1190
+ findLatestCompletedRunSummary(projectId, linearIssueId) {
1191
+ const runs = this.listRunsForIssue(projectId, linearIssueId);
1192
+ for (let index = runs.length - 1; index >= 0; index -= 1) {
1193
+ const run = runs[index];
1194
+ if (!run || run.status !== "completed") {
1195
+ continue;
1196
+ }
1197
+ const summary = extractLatestAssistantSummary(run);
1198
+ if (summary?.trim()) {
1199
+ return summary;
1200
+ }
1201
+ }
1202
+ return undefined;
1203
+ }
662
1204
  }
663
1205
  // ─── Row mappers ──────────────────────────────────────────────────
664
1206
  function mapIssueRow(row) {
@@ -680,18 +1222,23 @@ function mapIssueRow(row) {
680
1222
  ...(row.pending_run_type !== null && row.pending_run_type !== undefined ? { pendingRunType: String(row.pending_run_type) } : {}),
681
1223
  ...(row.pending_run_context_json !== null && row.pending_run_context_json !== undefined ? { pendingRunContextJson: String(row.pending_run_context_json) } : {}),
682
1224
  ...(row.branch_name !== null ? { branchName: String(row.branch_name) } : {}),
683
- ...(row.branch_owner !== null && row.branch_owner !== undefined ? { branchOwner: String(row.branch_owner) } : { branchOwner: "patchrelay" }),
1225
+ ...(row.branch_owner !== null && row.branch_owner !== undefined && String(row.branch_owner) === "patchrelay"
1226
+ ? { branchOwner: "patchrelay" }
1227
+ : { branchOwner: "patchrelay" }),
684
1228
  ...(row.branch_ownership_changed_at !== null && row.branch_ownership_changed_at !== undefined
685
1229
  ? { branchOwnershipChangedAt: String(row.branch_ownership_changed_at) }
686
1230
  : {}),
687
1231
  ...(row.worktree_path !== null ? { worktreePath: String(row.worktree_path) } : {}),
688
1232
  ...(row.thread_id !== null ? { threadId: String(row.thread_id) } : {}),
689
1233
  ...(row.active_run_id !== null ? { activeRunId: Number(row.active_run_id) } : {}),
1234
+ ...(row.status_comment_id !== null && row.status_comment_id !== undefined ? { statusCommentId: String(row.status_comment_id) } : {}),
690
1235
  ...(row.agent_session_id !== null ? { agentSessionId: String(row.agent_session_id) } : {}),
691
1236
  updatedAt: String(row.updated_at),
692
1237
  ...(row.pr_number !== null && row.pr_number !== undefined ? { prNumber: Number(row.pr_number) } : {}),
693
1238
  ...(row.pr_url !== null && row.pr_url !== undefined ? { prUrl: String(row.pr_url) } : {}),
694
1239
  ...(row.pr_state !== null && row.pr_state !== undefined ? { prState: String(row.pr_state) } : {}),
1240
+ ...(row.pr_head_sha !== null && row.pr_head_sha !== undefined ? { prHeadSha: String(row.pr_head_sha) } : {}),
1241
+ ...(row.pr_author_login !== null && row.pr_author_login !== undefined ? { prAuthorLogin: String(row.pr_author_login) } : {}),
695
1242
  ...(row.pr_review_state !== null && row.pr_review_state !== undefined ? { prReviewState: String(row.pr_review_state) } : {}),
696
1243
  ...(row.pr_check_status !== null && row.pr_check_status !== undefined ? { prCheckStatus: String(row.pr_check_status) } : {}),
697
1244
  ...(row.last_github_failure_source !== null && row.last_github_failure_source !== undefined
@@ -747,7 +1294,49 @@ function mapIssueRow(row) {
747
1294
  reviewFixAttempts: Number(row.review_fix_attempts ?? 0),
748
1295
  zombieRecoveryAttempts: Number(row.zombie_recovery_attempts ?? 0),
749
1296
  ...(row.last_zombie_recovery_at !== null && row.last_zombie_recovery_at !== undefined ? { lastZombieRecoveryAt: String(row.last_zombie_recovery_at) } : {}),
750
- queueLabelApplied: Boolean(row.queue_label_applied),
1297
+ };
1298
+ }
1299
+ function mapIssueSessionRow(row) {
1300
+ return {
1301
+ id: Number(row.id),
1302
+ projectId: String(row.project_id),
1303
+ linearIssueId: String(row.linear_issue_id),
1304
+ ...(row.issue_key !== null && row.issue_key !== undefined ? { issueKey: String(row.issue_key) } : {}),
1305
+ repoId: String(row.repo_id),
1306
+ ...(row.branch_name !== null && row.branch_name !== undefined ? { branchName: String(row.branch_name) } : {}),
1307
+ ...(row.worktree_path !== null && row.worktree_path !== undefined ? { worktreePath: String(row.worktree_path) } : {}),
1308
+ ...(row.pr_number !== null && row.pr_number !== undefined ? { prNumber: Number(row.pr_number) } : {}),
1309
+ ...(row.pr_head_sha !== null && row.pr_head_sha !== undefined ? { prHeadSha: String(row.pr_head_sha) } : {}),
1310
+ ...(row.pr_author_login !== null && row.pr_author_login !== undefined ? { prAuthorLogin: String(row.pr_author_login) } : {}),
1311
+ sessionState: String(row.session_state),
1312
+ ...(row.waiting_reason !== null && row.waiting_reason !== undefined ? { waitingReason: String(row.waiting_reason) } : {}),
1313
+ ...(row.summary_text !== null && row.summary_text !== undefined ? { summaryText: String(row.summary_text) } : {}),
1314
+ ...(row.active_thread_id !== null && row.active_thread_id !== undefined ? { activeThreadId: String(row.active_thread_id) } : {}),
1315
+ threadGeneration: Number(row.thread_generation ?? 0),
1316
+ ...(row.active_run_id !== null && row.active_run_id !== undefined ? { activeRunId: Number(row.active_run_id) } : {}),
1317
+ ...(row.last_run_type !== null && row.last_run_type !== undefined ? { lastRunType: String(row.last_run_type) } : {}),
1318
+ ...(row.last_wake_reason !== null && row.last_wake_reason !== undefined ? { lastWakeReason: String(row.last_wake_reason) } : {}),
1319
+ ciRepairAttempts: Number(row.ci_repair_attempts ?? 0),
1320
+ queueRepairAttempts: Number(row.queue_repair_attempts ?? 0),
1321
+ reviewFixAttempts: Number(row.review_fix_attempts ?? 0),
1322
+ ...(row.lease_id !== null && row.lease_id !== undefined ? { leaseId: String(row.lease_id) } : {}),
1323
+ ...(row.worker_id !== null && row.worker_id !== undefined ? { workerId: String(row.worker_id) } : {}),
1324
+ ...(row.leased_until !== null && row.leased_until !== undefined ? { leasedUntil: String(row.leased_until) } : {}),
1325
+ createdAt: String(row.created_at),
1326
+ updatedAt: String(row.updated_at),
1327
+ };
1328
+ }
1329
+ function mapIssueSessionEventRow(row) {
1330
+ return {
1331
+ id: Number(row.id),
1332
+ projectId: String(row.project_id),
1333
+ linearIssueId: String(row.linear_issue_id),
1334
+ eventType: String(row.event_type),
1335
+ ...(row.event_json !== null && row.event_json !== undefined ? { eventJson: String(row.event_json) } : {}),
1336
+ ...(row.dedupe_key !== null && row.dedupe_key !== undefined ? { dedupeKey: String(row.dedupe_key) } : {}),
1337
+ createdAt: String(row.created_at),
1338
+ ...(row.processed_at !== null && row.processed_at !== undefined ? { processedAt: String(row.processed_at) } : {}),
1339
+ ...(row.consumed_by_run_id !== null && row.consumed_by_run_id !== undefined ? { consumedByRunId: Number(row.consumed_by_run_id) } : {}),
751
1340
  };
752
1341
  }
753
1342
  function mapRunRow(row) {