patchrelay 0.36.7 → 0.36.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/db.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { isIssueSessionReadyForExecution, deriveIssueSessionState, deriveIssueSessionReactiveIntent, deriveIssueSessionWakeReason, } from "./issue-session.js";
2
- import { deriveSessionWakePlan, extractLatestAssistantSummary, } from "./issue-session-events.js";
3
- import { parseGitHubFailureContext } from "./github-failure-context.js";
4
- import { deriveIssueStatusNote } from "./status-note.js";
2
+ import { extractLatestAssistantSummary, } from "./issue-session-events.js";
3
+ import { IssueSessionStore } from "./db/issue-session-store.js";
5
4
  import { LinearInstallationStore } from "./db/linear-installation-store.js";
6
5
  import { OperatorFeedStore } from "./db/operator-feed-store.js";
7
6
  import { RepositoryLinkStore } from "./db/repository-link-store.js";
7
+ import { RunStore } from "./db/run-store.js";
8
+ import { WebhookEventStore } from "./db/webhook-event-store.js";
8
9
  import { runPatchRelayMigrations } from "./db/migrations.js";
9
10
  import { SqliteConnection, isoNow } from "./db/shared.js";
10
- import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
11
+ import { buildTrackedIssueRecord } from "./tracked-issue-projector.js";
11
12
  function parseObjectJson(raw) {
12
13
  if (!raw)
13
14
  return undefined;
@@ -90,6 +91,9 @@ export class PatchRelayDatabase {
90
91
  linearInstallations;
91
92
  operatorFeed;
92
93
  repositories;
94
+ webhookEvents;
95
+ issueSessions;
96
+ runs;
93
97
  constructor(databasePath, wal) {
94
98
  this.connection = new SqliteConnection(databasePath);
95
99
  this.connection.pragma("foreign_keys = ON");
@@ -99,6 +103,9 @@ export class PatchRelayDatabase {
99
103
  this.linearInstallations = new LinearInstallationStore(this.connection);
100
104
  this.operatorFeed = new OperatorFeedStore(this.connection);
101
105
  this.repositories = new RepositoryLinkStore(this.connection);
106
+ this.webhookEvents = new WebhookEventStore(this.connection);
107
+ this.issueSessions = new IssueSessionStore(this.connection, mapIssueSessionRow, mapIssueSessionEventRow, (projectId, linearIssueId) => this.getIssue(projectId, linearIssueId), deriveImplicitReactiveWake, (fn) => this.transaction(fn), (params) => this.upsertIssue(params), (runId, params) => this.runs.finishRun(runId, params), (runId, params) => this.runs.updateRunThread(runId, params), (projectId, linearIssueId, owner) => this.setBranchOwner(projectId, linearIssueId, owner));
108
+ this.runs = new RunStore(this.connection, mapRunRow, (id) => this.runs.getRunById(id), (projectId, linearIssueId) => this.getIssue(projectId, linearIssueId), (issue, options) => this.syncIssueSessionFromIssue(issue, options));
102
109
  }
103
110
  runMigrations() {
104
111
  runPatchRelayMigrations(this.connection);
@@ -106,46 +113,6 @@ export class PatchRelayDatabase {
106
113
  transaction(fn) {
107
114
  return this.connection.transaction(fn)();
108
115
  }
109
- // ─── Webhook Events ───────────────────────────────────────────────
110
- insertWebhookEvent(webhookId, receivedAt) {
111
- const existing = this.connection
112
- .prepare("SELECT id FROM webhook_events WHERE webhook_id = ?")
113
- .get(webhookId);
114
- if (existing) {
115
- return { id: existing.id, duplicate: true };
116
- }
117
- const result = this.connection
118
- .prepare("INSERT INTO webhook_events (webhook_id, received_at, processing_status) VALUES (?, ?, 'processed')")
119
- .run(webhookId, receivedAt);
120
- return { id: Number(result.lastInsertRowid), duplicate: false };
121
- }
122
- insertFullWebhookEvent(params) {
123
- const existing = this.connection
124
- .prepare("SELECT id FROM webhook_events WHERE webhook_id = ?")
125
- .get(params.webhookId);
126
- if (existing) {
127
- return { id: existing.id, dedupeStatus: "duplicate" };
128
- }
129
- const result = this.connection
130
- .prepare("INSERT INTO webhook_events (webhook_id, received_at, payload_json) VALUES (?, ?, ?)")
131
- .run(params.webhookId, params.receivedAt, params.payloadJson);
132
- return { id: Number(result.lastInsertRowid), dedupeStatus: "accepted" };
133
- }
134
- getWebhookPayload(id) {
135
- const row = this.connection.prepare("SELECT webhook_id, payload_json FROM webhook_events WHERE id = ?").get(id);
136
- if (!row || !row.payload_json)
137
- return undefined;
138
- return { webhookId: String(row.webhook_id), payloadJson: String(row.payload_json) };
139
- }
140
- isWebhookDuplicate(webhookId) {
141
- return this.connection.prepare("SELECT 1 FROM webhook_events WHERE webhook_id = ?").get(webhookId) !== undefined;
142
- }
143
- markWebhookProcessed(id, status) {
144
- this.connection.prepare("UPDATE webhook_events SET processing_status = ? WHERE id = ?").run(status, id);
145
- }
146
- assignWebhookProject(id, projectId) {
147
- this.connection.prepare("UPDATE webhook_events SET project_id = ? WHERE id = ?").run(projectId, id);
148
- }
149
116
  // ─── Issues ───────────────────────────────────────────────────────
150
117
  upsertIssue(params) {
151
118
  const now = isoNow();
@@ -451,254 +418,6 @@ export class PatchRelayDatabase {
451
418
  const row = this.connection.prepare("SELECT * FROM issues WHERE pr_number = ?").get(prNumber);
452
419
  return row ? mapIssueRow(row) : undefined;
453
420
  }
454
- getIssueSession(projectId, linearIssueId) {
455
- const row = this.connection
456
- .prepare("SELECT * FROM issue_sessions WHERE project_id = ? AND linear_issue_id = ?")
457
- .get(projectId, linearIssueId);
458
- return row ? mapIssueSessionRow(row) : undefined;
459
- }
460
- getIssueSessionByKey(issueKey) {
461
- const row = this.connection.prepare("SELECT * FROM issue_sessions WHERE issue_key = ?").get(issueKey);
462
- return row ? mapIssueSessionRow(row) : undefined;
463
- }
464
- appendIssueSessionEvent(params) {
465
- if (params.dedupeKey) {
466
- const existing = this.connection.prepare(`
467
- SELECT * FROM issue_session_events
468
- WHERE project_id = ? AND linear_issue_id = ? AND dedupe_key = ? AND processed_at IS NULL
469
- ORDER BY id DESC LIMIT 1
470
- `).get(params.projectId, params.linearIssueId, params.dedupeKey);
471
- if (existing)
472
- return mapIssueSessionEventRow(existing);
473
- }
474
- const now = isoNow();
475
- const result = this.connection.prepare(`
476
- INSERT INTO issue_session_events (
477
- project_id, linear_issue_id, event_type, event_json, dedupe_key, created_at
478
- ) VALUES (?, ?, ?, ?, ?, ?)
479
- `).run(params.projectId, params.linearIssueId, params.eventType, params.eventJson ?? null, params.dedupeKey ?? null, now);
480
- return this.getIssueSessionEvent(Number(result.lastInsertRowid));
481
- }
482
- appendIssueSessionEventWithLease(lease, params) {
483
- return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => this.appendIssueSessionEvent(params));
484
- }
485
- appendIssueSessionEventRespectingActiveLease(projectId, linearIssueId, params) {
486
- const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
487
- if (!lease) {
488
- return this.appendIssueSessionEvent(params);
489
- }
490
- return this.appendIssueSessionEventWithLease(lease, params);
491
- }
492
- getIssueSessionEvent(id) {
493
- const row = this.connection.prepare("SELECT * FROM issue_session_events WHERE id = ?").get(id);
494
- return row ? mapIssueSessionEventRow(row) : undefined;
495
- }
496
- listIssueSessionEvents(projectId, linearIssueId, options) {
497
- const conditions = ["project_id = ?", "linear_issue_id = ?"];
498
- const values = [projectId, linearIssueId];
499
- if (options?.pendingOnly) {
500
- conditions.push("processed_at IS NULL");
501
- }
502
- let query = `SELECT * FROM issue_session_events WHERE ${conditions.join(" AND ")} ORDER BY id`;
503
- if (options?.limit !== undefined) {
504
- query += " LIMIT ?";
505
- values.push(options.limit);
506
- }
507
- const rows = this.connection.prepare(query).all(...values);
508
- return rows.map(mapIssueSessionEventRow);
509
- }
510
- consumeIssueSessionEvents(projectId, linearIssueId, eventIds, runId) {
511
- if (eventIds.length === 0)
512
- return;
513
- const now = isoNow();
514
- const placeholders = eventIds.map(() => "?").join(", ");
515
- this.connection.prepare(`
516
- UPDATE issue_session_events
517
- SET processed_at = ?, consumed_by_run_id = ?
518
- WHERE project_id = ? AND linear_issue_id = ? AND id IN (${placeholders}) AND processed_at IS NULL
519
- `).run(now, runId, projectId, linearIssueId, ...eventIds);
520
- }
521
- clearPendingIssueSessionEvents(projectId, linearIssueId) {
522
- this.connection.prepare(`
523
- UPDATE issue_session_events
524
- SET processed_at = ?, consumed_by_run_id = NULL
525
- WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
526
- `).run(isoNow(), projectId, linearIssueId);
527
- }
528
- hasPendingIssueSessionEvents(projectId, linearIssueId) {
529
- const row = this.connection.prepare(`
530
- SELECT 1
531
- FROM issue_session_events
532
- WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
533
- LIMIT 1
534
- `).get(projectId, linearIssueId);
535
- return row !== undefined;
536
- }
537
- peekIssueSessionWake(projectId, linearIssueId) {
538
- const issue = this.getIssue(projectId, linearIssueId);
539
- if (!issue)
540
- return undefined;
541
- const events = this.listIssueSessionEvents(projectId, linearIssueId, { pendingOnly: true });
542
- const plan = deriveSessionWakePlan(issue, events);
543
- if (plan?.runType) {
544
- return {
545
- eventIds: events.map((event) => event.id),
546
- runType: plan.runType,
547
- context: plan.context,
548
- ...(plan.wakeReason ? { wakeReason: plan.wakeReason } : {}),
549
- resumeThread: plan.resumeThread,
550
- };
551
- }
552
- const implicitWake = deriveImplicitReactiveWake(issue);
553
- if (!implicitWake)
554
- return undefined;
555
- return {
556
- eventIds: [],
557
- runType: implicitWake.runType,
558
- context: implicitWake.context,
559
- wakeReason: implicitWake.wakeReason,
560
- resumeThread: false,
561
- };
562
- }
563
- acquireIssueSessionLease(params) {
564
- const now = params.now ?? isoNow();
565
- const result = this.connection.prepare(`
566
- UPDATE issue_sessions
567
- SET lease_id = ?, worker_id = ?, leased_until = ?, updated_at = ?
568
- WHERE project_id = ? AND linear_issue_id = ?
569
- AND (leased_until IS NULL OR leased_until <= ? OR lease_id = ?)
570
- `).run(params.leaseId, params.workerId, params.leasedUntil, now, params.projectId, params.linearIssueId, now, params.leaseId);
571
- return Number(result.changes ?? 0) > 0;
572
- }
573
- forceAcquireIssueSessionLease(params) {
574
- const now = params.now ?? isoNow();
575
- const result = this.connection.prepare(`
576
- UPDATE issue_sessions
577
- SET lease_id = ?, worker_id = ?, leased_until = ?, updated_at = ?
578
- WHERE project_id = ? AND linear_issue_id = ?
579
- `).run(params.leaseId, params.workerId, params.leasedUntil, now, params.projectId, params.linearIssueId);
580
- return Number(result.changes ?? 0) > 0;
581
- }
582
- renewIssueSessionLease(params) {
583
- const now = params.now ?? isoNow();
584
- const result = this.connection.prepare(`
585
- UPDATE issue_sessions
586
- SET leased_until = ?, updated_at = ?
587
- WHERE project_id = ? AND linear_issue_id = ? AND lease_id = ?
588
- `).run(params.leasedUntil, now, params.projectId, params.linearIssueId, params.leaseId);
589
- return Number(result.changes ?? 0) > 0;
590
- }
591
- releaseIssueSessionLease(projectId, linearIssueId, leaseId) {
592
- this.connection.prepare(`
593
- UPDATE issue_sessions
594
- SET lease_id = NULL, worker_id = NULL, leased_until = NULL, updated_at = ?
595
- WHERE project_id = ? AND linear_issue_id = ? AND (? IS NULL OR lease_id = ?)
596
- `).run(isoNow(), projectId, linearIssueId, leaseId ?? null, leaseId ?? null);
597
- }
598
- releaseExpiredIssueSessionLeases(now = isoNow()) {
599
- this.connection.prepare(`
600
- UPDATE issue_sessions
601
- SET lease_id = NULL, worker_id = NULL, leased_until = NULL, updated_at = ?
602
- WHERE leased_until IS NOT NULL AND leased_until <= ?
603
- `).run(now, now);
604
- }
605
- hasActiveIssueSessionLease(projectId, linearIssueId, leaseId, now = isoNow()) {
606
- const row = this.connection.prepare(`
607
- SELECT 1
608
- FROM issue_sessions
609
- WHERE project_id = ? AND linear_issue_id = ? AND lease_id = ?
610
- AND leased_until IS NOT NULL
611
- AND leased_until > ?
612
- LIMIT 1
613
- `).get(projectId, linearIssueId, leaseId, now);
614
- return row !== undefined;
615
- }
616
- getActiveIssueSessionLease(projectId, linearIssueId, now = isoNow()) {
617
- const row = this.connection.prepare(`
618
- SELECT lease_id
619
- FROM issue_sessions
620
- WHERE project_id = ? AND linear_issue_id = ?
621
- AND lease_id IS NOT NULL
622
- AND leased_until IS NOT NULL
623
- AND leased_until > ?
624
- LIMIT 1
625
- `).get(projectId, linearIssueId, now);
626
- const leaseId = typeof row?.lease_id === "string" ? row.lease_id : undefined;
627
- if (!leaseId)
628
- return undefined;
629
- return { projectId, linearIssueId, leaseId };
630
- }
631
- withIssueSessionLease(projectId, linearIssueId, leaseId, fn) {
632
- return this.transaction(() => {
633
- if (!this.hasActiveIssueSessionLease(projectId, linearIssueId, leaseId)) {
634
- return undefined;
635
- }
636
- return fn();
637
- });
638
- }
639
- upsertIssueWithLease(lease, params) {
640
- return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => this.upsertIssue(params));
641
- }
642
- upsertIssueRespectingActiveLease(projectId, linearIssueId, params) {
643
- const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
644
- if (!lease) {
645
- return this.upsertIssue(params);
646
- }
647
- return this.upsertIssueWithLease(lease, params);
648
- }
649
- finishRunWithLease(lease, runId, params) {
650
- return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
651
- this.finishRun(runId, params);
652
- return true;
653
- }) ?? false;
654
- }
655
- finishRunRespectingActiveLease(projectId, linearIssueId, runId, params) {
656
- const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
657
- if (!lease) {
658
- this.finishRun(runId, params);
659
- return true;
660
- }
661
- return this.finishRunWithLease(lease, runId, params);
662
- }
663
- updateRunThreadWithLease(lease, runId, params) {
664
- return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
665
- this.updateRunThread(runId, params);
666
- return true;
667
- }) ?? false;
668
- }
669
- consumeIssueSessionEventsWithLease(lease, eventIds, runId) {
670
- return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
671
- this.consumeIssueSessionEvents(lease.projectId, lease.linearIssueId, eventIds, runId);
672
- return true;
673
- }) ?? false;
674
- }
675
- clearPendingIssueSessionEventsWithLease(lease) {
676
- return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
677
- this.clearPendingIssueSessionEvents(lease.projectId, lease.linearIssueId);
678
- return true;
679
- }) ?? false;
680
- }
681
- clearPendingIssueSessionEventsRespectingActiveLease(projectId, linearIssueId) {
682
- const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
683
- if (!lease) {
684
- this.clearPendingIssueSessionEvents(projectId, linearIssueId);
685
- return true;
686
- }
687
- return this.clearPendingIssueSessionEventsWithLease(lease);
688
- }
689
- setIssueSessionLastWakeReasonWithLease(lease, lastWakeReason) {
690
- return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
691
- this.setIssueSessionLastWakeReason(lease.projectId, lease.linearIssueId, lastWakeReason);
692
- return true;
693
- }) ?? false;
694
- }
695
- setIssueSessionLastWakeReason(projectId, linearIssueId, lastWakeReason) {
696
- this.connection.prepare(`
697
- UPDATE issue_sessions
698
- SET last_wake_reason = ?, updated_at = ?
699
- WHERE project_id = ? AND linear_issue_id = ?
700
- `).run(lastWakeReason ?? null, isoNow(), projectId, linearIssueId);
701
- }
702
421
  setBranchOwner(projectId, linearIssueId, owner) {
703
422
  this.connection.prepare(`
704
423
  UPDATE issues
@@ -706,24 +425,6 @@ export class PatchRelayDatabase {
706
425
  WHERE project_id = ? AND linear_issue_id = ?
707
426
  `).run(owner, isoNow(), isoNow(), projectId, linearIssueId);
708
427
  }
709
- setBranchOwnerWithLease(lease, owner) {
710
- return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
711
- this.setBranchOwner(lease.projectId, lease.linearIssueId, owner);
712
- return true;
713
- }) ?? false;
714
- }
715
- setBranchOwnerRespectingActiveLease(projectId, linearIssueId, owner) {
716
- const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
717
- if (!lease) {
718
- this.setBranchOwner(projectId, linearIssueId, owner);
719
- return true;
720
- }
721
- return this.setBranchOwnerWithLease(lease, owner);
722
- }
723
- releaseIssueSessionLeaseRespectingActiveLease(projectId, linearIssueId) {
724
- const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
725
- this.releaseIssueSessionLease(projectId, linearIssueId, lease?.leaseId);
726
- }
727
428
  replaceIssueDependencies(params) {
728
429
  const now = isoNow();
729
430
  this.connection
@@ -829,7 +530,7 @@ export class PatchRelayDatabase {
829
530
  }),
830
531
  activeRunId: issue.activeRunId,
831
532
  blockedByCount: this.countUnresolvedBlockers(issue.projectId, issue.linearIssueId),
832
- hasPendingWake: this.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined,
533
+ hasPendingWake: this.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined,
833
534
  hasLegacyPendingRun: issue.pendingRunType !== undefined,
834
535
  prNumber: issue.prNumber,
835
536
  prState: issue.prState,
@@ -890,186 +591,16 @@ export class PatchRelayDatabase {
890
591
  .all(projectId, state);
891
592
  return rows.map(mapIssueRow);
892
593
  }
893
- // ─── Runs ─────────────────────────────────────────────────────────
894
- createRun(params) {
895
- const now = isoNow();
896
- const result = this.connection.prepare(`
897
- INSERT INTO runs (issue_id, project_id, linear_issue_id, run_type, status, source_head_sha, prompt_text, started_at)
898
- VALUES (?, ?, ?, ?, 'queued', ?, ?, ?)
899
- `).run(params.issueId, params.projectId, params.linearIssueId, params.runType, params.sourceHeadSha ?? null, params.promptText ?? null, now);
900
- const run = this.getRun(Number(result.lastInsertRowid));
901
- const issue = this.getIssue(params.projectId, params.linearIssueId);
902
- if (issue) {
903
- this.syncIssueSessionFromIssue(issue, { lastRunType: run.runType });
904
- }
905
- return run;
906
- }
907
- getRun(id) {
908
- const row = this.connection.prepare("SELECT * FROM runs WHERE id = ?").get(id);
909
- return row ? mapRunRow(row) : undefined;
910
- }
911
- getRunByThreadId(threadId) {
912
- const row = this.connection.prepare("SELECT * FROM runs WHERE thread_id = ?").get(threadId);
913
- return row ? mapRunRow(row) : undefined;
914
- }
915
- listRunsForIssue(projectId, linearIssueId) {
916
- const rows = this.connection
917
- .prepare("SELECT * FROM runs WHERE project_id = ? AND linear_issue_id = ? ORDER BY id")
918
- .all(projectId, linearIssueId);
919
- return rows.map(mapRunRow);
920
- }
921
- getLatestRunForIssue(projectId, linearIssueId) {
922
- const row = this.connection
923
- .prepare("SELECT * FROM runs WHERE project_id = ? AND linear_issue_id = ? ORDER BY id DESC LIMIT 1")
924
- .get(projectId, linearIssueId);
925
- return row ? mapRunRow(row) : undefined;
926
- }
927
- listActiveRuns() {
928
- const rows = this.connection
929
- .prepare("SELECT * FROM runs WHERE status IN ('queued', 'running')")
930
- .all();
931
- return rows.map(mapRunRow);
932
- }
933
- listRunningRuns() {
934
- const rows = this.connection
935
- .prepare("SELECT * FROM runs WHERE status IN ('running', 'queued')")
936
- .all();
937
- return rows.map(mapRunRow);
938
- }
939
- updateRunThread(runId, params) {
940
- this.connection.prepare(`
941
- UPDATE runs SET
942
- thread_id = ?,
943
- parent_thread_id = COALESCE(?, parent_thread_id),
944
- turn_id = COALESCE(?, turn_id),
945
- status = 'running'
946
- WHERE id = ?
947
- AND ended_at IS NULL
948
- AND status IN ('queued', 'running')
949
- `).run(params.threadId, params.parentThreadId ?? null, params.turnId ?? null, runId);
950
- const run = this.getRun(runId);
951
- if (!run)
952
- return;
953
- const issue = this.getIssue(run.projectId, run.linearIssueId);
954
- if (issue) {
955
- this.syncIssueSessionFromIssue(issue);
956
- }
957
- }
958
- updateRunTurnId(runId, turnId) {
959
- this.connection.prepare("UPDATE runs SET turn_id = ? WHERE id = ?").run(turnId, runId);
960
- }
961
- finishRun(runId, params) {
962
- const now = isoNow();
963
- this.connection.prepare(`
964
- UPDATE runs SET
965
- status = ?,
966
- thread_id = COALESCE(?, thread_id),
967
- turn_id = COALESCE(?, turn_id),
968
- failure_reason = COALESCE(?, failure_reason),
969
- summary_json = COALESCE(?, summary_json),
970
- report_json = COALESCE(?, report_json),
971
- ended_at = ?
972
- WHERE id = ?
973
- `).run(params.status, params.threadId ?? null, params.turnId ?? null, params.failureReason ?? null, params.summaryJson ?? null, params.reportJson ?? null, now, runId);
974
- const run = this.getRun(runId);
975
- if (!run)
976
- return;
977
- const issue = this.getIssue(run.projectId, run.linearIssueId);
978
- if (issue) {
979
- this.syncIssueSessionFromIssue(issue, {
980
- summaryText: extractLatestAssistantSummary(this.getRun(runId) ?? run),
981
- lastRunType: run.runType,
982
- });
983
- }
984
- }
985
- // ─── Thread Events (kept for extended history) ────────────────────
986
- saveThreadEvent(params) {
987
- this.connection.prepare(`
988
- INSERT INTO run_thread_events (run_id, thread_id, turn_id, method, event_json, created_at)
989
- VALUES (?, ?, ?, ?, ?, ?)
990
- `).run(params.runId, params.threadId, params.turnId ?? null, params.method, params.eventJson, isoNow());
991
- }
992
- listThreadEvents(runId) {
993
- const rows = this.connection
994
- .prepare("SELECT * FROM run_thread_events WHERE run_id = ? ORDER BY id")
995
- .all(runId);
996
- return rows.map((row) => ({
997
- id: Number(row.id),
998
- runId: Number(row.run_id),
999
- threadId: String(row.thread_id),
1000
- ...(row.turn_id !== null ? { turnId: String(row.turn_id) } : {}),
1001
- method: String(row.method),
1002
- eventJson: String(row.event_json),
1003
- createdAt: String(row.created_at),
1004
- }));
1005
- }
1006
594
  // ─── View builders ──────────────────────────────────────────────
1007
595
  issueToTrackedIssue(issue) {
1008
- const session = this.getIssueSession(issue.projectId, issue.linearIssueId);
1009
- const blockedBy = this.listIssueDependencies(issue.projectId, issue.linearIssueId);
1010
- const unresolvedBlockedBy = blockedBy.filter((entry) => !isResolvedLinearState(entry.blockerCurrentLinearStateType, entry.blockerCurrentLinearState));
1011
- const pendingWake = this.peekIssueSessionWake(issue.projectId, issue.linearIssueId);
1012
- const failureContext = parseGitHubFailureContext(issue.lastGitHubFailureContextJson);
1013
- const blockedByKeys = unresolvedBlockedBy.map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId);
1014
- const waitingReason = derivePatchRelayWaitingReason({
1015
- ...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
1016
- blockedByKeys,
1017
- factoryState: issue.factoryState,
1018
- pendingRunType: issue.pendingRunType,
1019
- prNumber: issue.prNumber,
1020
- prHeadSha: issue.prHeadSha,
1021
- prReviewState: issue.prReviewState,
1022
- prCheckStatus: issue.prCheckStatus,
1023
- lastBlockingReviewHeadSha: issue.lastBlockingReviewHeadSha,
1024
- latestFailureCheckName: issue.lastGitHubFailureCheckName,
1025
- });
1026
- const latestRun = this.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
1027
- const latestEvent = this.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1);
1028
- const statusNote = deriveIssueStatusNote({
596
+ return buildTrackedIssueRecord({
1029
597
  issue,
1030
- sessionSummary: session?.summaryText,
1031
- latestRun,
1032
- latestEvent,
1033
- failureSummary: failureContext?.summary,
1034
- blockedByKeys,
1035
- waitingReason,
598
+ session: this.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId),
599
+ blockedBy: this.listIssueDependencies(issue.projectId, issue.linearIssueId),
600
+ hasPendingWake: this.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined,
601
+ latestRun: this.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId),
602
+ latestEvent: this.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1),
1036
603
  });
1037
- return {
1038
- id: issue.id,
1039
- projectId: issue.projectId,
1040
- linearIssueId: issue.linearIssueId,
1041
- ...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
1042
- ...(issue.title ? { title: issue.title } : {}),
1043
- ...(issue.url ? { issueUrl: issue.url } : {}),
1044
- ...(statusNote ? { statusNote } : {}),
1045
- ...(issue.currentLinearState ? { currentLinearState: issue.currentLinearState } : {}),
1046
- ...(session?.sessionState ? { sessionState: session.sessionState } : {}),
1047
- factoryState: issue.factoryState,
1048
- blockedByCount: unresolvedBlockedBy.length,
1049
- blockedByKeys,
1050
- readyForExecution: isIssueSessionReadyForExecution({
1051
- sessionState: session?.sessionState,
1052
- factoryState: issue.factoryState,
1053
- activeRunId: issue.activeRunId,
1054
- blockedByCount: unresolvedBlockedBy.length,
1055
- hasPendingWake: pendingWake !== undefined,
1056
- hasLegacyPendingRun: issue.pendingRunType !== undefined,
1057
- ...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
1058
- ...(issue.prState ? { prState: issue.prState } : {}),
1059
- ...(issue.prReviewState ? { prReviewState: issue.prReviewState } : {}),
1060
- ...(issue.prCheckStatus ? { prCheckStatus: issue.prCheckStatus } : {}),
1061
- ...(issue.lastGitHubFailureSource ? { latestFailureSource: issue.lastGitHubFailureSource } : {}),
1062
- }),
1063
- ...(issue.lastGitHubFailureSource ? { latestFailureSource: issue.lastGitHubFailureSource } : {}),
1064
- ...(issue.lastGitHubFailureHeadSha ? { latestFailureHeadSha: issue.lastGitHubFailureHeadSha } : {}),
1065
- ...(issue.lastGitHubFailureCheckName ? { latestFailureCheckName: issue.lastGitHubFailureCheckName } : {}),
1066
- ...(failureContext?.stepName ? { latestFailureStepName: failureContext.stepName } : {}),
1067
- ...(failureContext?.summary ? { latestFailureSummary: failureContext.summary } : {}),
1068
- ...(waitingReason ? { waitingReason } : {}),
1069
- ...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
1070
- ...(issue.agentSessionId ? { activeAgentSessionId: issue.agentSessionId } : {}),
1071
- updatedAt: issue.updatedAt,
1072
- };
1073
604
  }
1074
605
  getTrackedIssue(projectId, linearIssueId) {
1075
606
  const issue = this.getIssue(projectId, linearIssueId);
@@ -1091,39 +622,13 @@ export class PatchRelayDatabase {
1091
622
  .all();
1092
623
  return rows.map(mapIssueRow);
1093
624
  }
1094
- findLatestAgentSessionIdForIssue(linearIssueId) {
1095
- const row = this.connection.prepare(`
1096
- SELECT COALESCE(
1097
- json_extract(payload_json, '$.agentSession.id'),
1098
- json_extract(payload_json, '$.data.agentSession.id'),
1099
- json_extract(payload_json, '$.agentSessionId'),
1100
- json_extract(payload_json, '$.data.agentSessionId')
1101
- ) AS agent_session_id
1102
- FROM webhook_events
1103
- WHERE COALESCE(
1104
- json_extract(payload_json, '$.agentSession.issueId'),
1105
- json_extract(payload_json, '$.data.agentSession.issueId'),
1106
- json_extract(payload_json, '$.agentSession.issue.id'),
1107
- json_extract(payload_json, '$.data.agentSession.issue.id')
1108
- ) = ?
1109
- AND COALESCE(
1110
- json_extract(payload_json, '$.agentSession.id'),
1111
- json_extract(payload_json, '$.data.agentSession.id'),
1112
- json_extract(payload_json, '$.agentSessionId'),
1113
- json_extract(payload_json, '$.data.agentSessionId')
1114
- ) IS NOT NULL
1115
- ORDER BY id DESC
1116
- LIMIT 1
1117
- `).get(linearIssueId);
1118
- return row?.agent_session_id != null ? String(row.agent_session_id) : undefined;
1119
- }
1120
625
  // ─── Issue overview for query service ─────────────────────────────
1121
626
  getIssueOverview(issueKey) {
1122
627
  const issue = this.getIssueByKey(issueKey);
1123
628
  if (!issue)
1124
629
  return undefined;
1125
630
  const tracked = this.issueToTrackedIssue(issue);
1126
- const activeRun = issue.activeRunId ? this.getRun(issue.activeRunId) : undefined;
631
+ const activeRun = issue.activeRunId ? this.runs.getRunById(issue.activeRunId) : undefined;
1127
632
  return {
1128
633
  issue: tracked,
1129
634
  ...(activeRun ? { activeRun } : {}),
@@ -1131,8 +636,8 @@ export class PatchRelayDatabase {
1131
636
  }
1132
637
  syncIssueSessionFromIssue(issue, options) {
1133
638
  const tracked = this.issueToTrackedIssue(issue);
1134
- const existing = this.getIssueSession(issue.projectId, issue.linearIssueId);
1135
- const latestRun = this.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
639
+ const existing = this.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId);
640
+ const latestRun = this.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
1136
641
  const latestRunType = options?.lastRunType ?? latestRun?.runType ?? existing?.lastRunType;
1137
642
  const summaryText = this.resolveIssueSessionSummary(issue, latestRun, existing?.summaryText, options?.summaryText);
1138
643
  const activeThreadId = issue.threadId ?? existing?.activeThreadId;
@@ -1218,7 +723,7 @@ export class PatchRelayDatabase {
1218
723
  || issue.factoryState === "done";
1219
724
  }
1220
725
  findLatestCompletedRunSummary(projectId, linearIssueId) {
1221
- const runs = this.listRunsForIssue(projectId, linearIssueId);
726
+ const runs = this.runs.listRunsForIssue(projectId, linearIssueId);
1222
727
  for (let index = runs.length - 1; index >= 0; index -= 1) {
1223
728
  const run = runs[index];
1224
729
  if (!run || run.status !== "completed") {
@@ -1392,6 +897,3 @@ function mapRunRow(row) {
1392
897
  ...(row.ended_at !== null ? { endedAt: String(row.ended_at) } : {}),
1393
898
  };
1394
899
  }
1395
- function isResolvedLinearState(stateType, stateName) {
1396
- return stateType === "completed" || stateName?.trim().toLowerCase() === "done";
1397
- }