patchrelay 0.36.11 → 0.36.13

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.11",
4
- "commit": "ed9637ca51b3",
5
- "builtAt": "2026-04-09T20:38:07.717Z"
3
+ "version": "0.36.13",
4
+ "commit": "e015f208d2a5",
5
+ "builtAt": "2026-04-10T03:05:43.066Z"
6
6
  }
package/dist/db.js CHANGED
@@ -1,4 +1,4 @@
1
- import { isIssueSessionReadyForExecution, deriveIssueSessionState, deriveIssueSessionReactiveIntent, } from "./issue-session.js";
1
+ import { deriveIssueSessionReactiveIntent, } from "./issue-session.js";
2
2
  import {} from "./issue-session-events.js";
3
3
  import { IssueStore } from "./db/issue-store.js";
4
4
  import { IssueSessionStore } from "./db/issue-session-store.js";
@@ -10,7 +10,7 @@ import { WebhookEventStore } from "./db/webhook-event-store.js";
10
10
  import { runPatchRelayMigrations } from "./db/migrations.js";
11
11
  import { SqliteConnection } from "./db/shared.js";
12
12
  import { syncIssueSessionFromIssue } from "./issue-session-projector.js";
13
- import { buildTrackedIssueRecord } from "./tracked-issue-projector.js";
13
+ import { TrackedIssueQuery } from "./tracked-issue-query.js";
14
14
  function parseObjectJson(raw) {
15
15
  if (!raw)
16
16
  return undefined;
@@ -97,6 +97,7 @@ export class PatchRelayDatabase {
97
97
  issues;
98
98
  issueSessions;
99
99
  runs;
100
+ trackedIssues;
100
101
  constructor(databasePath, wal) {
101
102
  this.connection = new SqliteConnection(databasePath);
102
103
  this.connection.pragma("foreign_keys = ON");
@@ -117,6 +118,7 @@ export class PatchRelayDatabase {
117
118
  ...(options ? { options } : {}),
118
119
  }));
119
120
  this.issueSessions = new IssueSessionStore(this.connection, mapIssueSessionRow, mapIssueSessionEventRow, this.issues, this.runs, deriveImplicitReactiveWake);
121
+ this.trackedIssues = new TrackedIssueQuery(this.issues, this.issueSessions, this.runs);
120
122
  }
121
123
  runMigrations() {
122
124
  runPatchRelayMigrations(this.connection);
@@ -161,27 +163,7 @@ export class PatchRelayDatabase {
161
163
  return this.issues.countUnresolvedBlockers(projectId, linearIssueId);
162
164
  }
163
165
  listIssuesReadyForExecution() {
164
- return this.issues.listIssues()
165
- .filter((issue) => isIssueSessionReadyForExecution({
166
- factoryState: issue.factoryState,
167
- sessionState: deriveIssueSessionState({
168
- activeRunId: issue.activeRunId,
169
- factoryState: issue.factoryState,
170
- }),
171
- activeRunId: issue.activeRunId,
172
- blockedByCount: this.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId),
173
- hasPendingWake: this.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined,
174
- hasLegacyPendingRun: issue.pendingRunType !== undefined,
175
- prNumber: issue.prNumber,
176
- prState: issue.prState,
177
- prReviewState: issue.prReviewState,
178
- prCheckStatus: issue.prCheckStatus,
179
- latestFailureSource: issue.lastGitHubFailureSource,
180
- }))
181
- .map((issue) => ({
182
- projectId: issue.projectId,
183
- linearIssueId: issue.linearIssueId,
184
- }));
166
+ return this.trackedIssues.listIssuesReadyForExecution();
185
167
  }
186
168
  /**
187
169
  * Issues idle in pr_open with no active run — candidates for state
@@ -205,26 +187,17 @@ export class PatchRelayDatabase {
205
187
  return this.issues.listAwaitingQueueIssues();
206
188
  }
207
189
  listIssuesByState(projectId, state) {
208
- return this.issues.listIssuesByState(projectId, state);
190
+ return this.trackedIssues.listIssuesByState(projectId, state);
209
191
  }
210
192
  // ─── View builders ──────────────────────────────────────────────
211
193
  issueToTrackedIssue(issue) {
212
- return buildTrackedIssueRecord({
213
- issue,
214
- session: this.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId),
215
- blockedBy: this.issues.listIssueDependencies(issue.projectId, issue.linearIssueId),
216
- hasPendingWake: this.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined,
217
- latestRun: this.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId),
218
- latestEvent: this.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1),
219
- });
194
+ return this.trackedIssues.issueToTrackedIssue(issue);
220
195
  }
221
196
  getTrackedIssue(projectId, linearIssueId) {
222
- const issue = this.getIssue(projectId, linearIssueId);
223
- return issue ? this.issueToTrackedIssue(issue) : undefined;
197
+ return this.trackedIssues.getTrackedIssue(projectId, linearIssueId);
224
198
  }
225
199
  getTrackedIssueByKey(issueKey) {
226
- const issue = this.getIssueByKey(issueKey);
227
- return issue ? this.issueToTrackedIssue(issue) : undefined;
200
+ return this.trackedIssues.getTrackedIssueByKey(issueKey);
228
201
  }
229
202
  listIssues() {
230
203
  return this.issues.listIssues();
@@ -234,15 +207,7 @@ export class PatchRelayDatabase {
234
207
  }
235
208
  // ─── Issue overview for query service ─────────────────────────────
236
209
  getIssueOverview(issueKey) {
237
- const issue = this.getIssueByKey(issueKey);
238
- if (!issue)
239
- return undefined;
240
- const tracked = this.issueToTrackedIssue(issue);
241
- const activeRun = issue.activeRunId ? this.runs.getRunById(issue.activeRunId) : undefined;
242
- return {
243
- issue: tracked,
244
- ...(activeRun ? { activeRun } : {}),
245
- };
210
+ return this.trackedIssues.getIssueOverview(issueKey);
246
211
  }
247
212
  }
248
213
  // ─── Row mappers ──────────────────────────────────────────────────
@@ -0,0 +1,127 @@
1
+ import { execCommand } from "./utils.js";
2
+ import { resolveImplementationDeliveryMode, } from "./prompting/patchrelay.js";
3
+ export class ImplementationOutcomePolicy {
4
+ config;
5
+ db;
6
+ logger;
7
+ withHeldLease;
8
+ constructor(config, db, logger, withHeldLease) {
9
+ this.config = config;
10
+ this.db = db;
11
+ this.logger = logger;
12
+ this.withHeldLease = withHeldLease;
13
+ }
14
+ async verifyPublishedRunOutcome(run, issue) {
15
+ if (run.runType !== "implementation") {
16
+ return undefined;
17
+ }
18
+ const project = this.config.projects.find((entry) => entry.id === run.projectId);
19
+ const baseBranch = project?.github?.baseBranch ?? "main";
20
+ const deliveryMode = resolveImplementationDeliveryMode(issue, undefined, run.promptText);
21
+ if (deliveryMode === "linear_only") {
22
+ if (issue.prNumber !== undefined) {
23
+ return `Planning-only implementation should not open a PR, but PR #${issue.prNumber} was observed`;
24
+ }
25
+ return this.describeLocalImplementationOutcome(issue, baseBranch, deliveryMode);
26
+ }
27
+ if (issue.prNumber && issue.prState && issue.prState !== "closed") {
28
+ return undefined;
29
+ }
30
+ if (project?.github?.repoFullName && issue.branchName) {
31
+ try {
32
+ const { stdout, exitCode } = await execCommand("gh", [
33
+ "pr",
34
+ "list",
35
+ "--repo",
36
+ project.github.repoFullName,
37
+ "--head",
38
+ issue.branchName,
39
+ "--state",
40
+ "all",
41
+ "--json",
42
+ "number,url,state,author,headRefOid",
43
+ ], { timeoutMs: 10_000 });
44
+ if (exitCode === 0) {
45
+ const matches = JSON.parse(stdout);
46
+ const pr = matches[0];
47
+ if (pr?.number) {
48
+ this.upsertIssueIfLeaseHeld(issue.projectId, issue.linearIssueId, {
49
+ projectId: issue.projectId,
50
+ linearIssueId: issue.linearIssueId,
51
+ prNumber: pr.number,
52
+ ...(pr.url ? { prUrl: pr.url } : {}),
53
+ ...(pr.state ? { prState: pr.state.toLowerCase() } : {}),
54
+ ...(pr.headRefOid ? { prHeadSha: pr.headRefOid } : {}),
55
+ ...(pr.author?.login ? { prAuthorLogin: pr.author.login } : {}),
56
+ }, "published PR verification refresh");
57
+ return undefined;
58
+ }
59
+ }
60
+ }
61
+ catch (error) {
62
+ this.logger.debug({
63
+ issueKey: issue.issueKey,
64
+ branchName: issue.branchName,
65
+ repoFullName: project.github.repoFullName,
66
+ error: error instanceof Error ? error.message : String(error),
67
+ }, "Failed to verify published PR state after implementation");
68
+ }
69
+ }
70
+ const details = await this.describeLocalImplementationOutcome(issue, baseBranch, deliveryMode);
71
+ return details ?? `Implementation completed without opening a PR for branch ${issue.branchName ?? issue.linearIssueId}`;
72
+ }
73
+ upsertIssueIfLeaseHeld(projectId, linearIssueId, params, context) {
74
+ const updated = this.withHeldLease(projectId, linearIssueId, (lease) => this.db.issueSessions.upsertIssueWithLease(lease, params));
75
+ if (updated === undefined) {
76
+ this.logger.warn({ projectId, linearIssueId, context }, "Skipping issue write after losing issue-session lease");
77
+ }
78
+ return updated;
79
+ }
80
+ async describeLocalImplementationOutcome(issue, baseBranch, deliveryMode = "publish_pr") {
81
+ if (!issue.worktreePath) {
82
+ return undefined;
83
+ }
84
+ try {
85
+ const status = await execCommand(this.config.runner.gitBin, [
86
+ "-C",
87
+ issue.worktreePath,
88
+ "status",
89
+ "--short",
90
+ ], { timeoutMs: 10_000 });
91
+ const dirtyEntries = status.exitCode === 0
92
+ ? status.stdout.split("\n").map((line) => line.trim()).filter(Boolean)
93
+ : [];
94
+ if (dirtyEntries.length > 0) {
95
+ if (deliveryMode === "linear_only") {
96
+ return `Planning-only implementation should not modify the repo; worktree still has ${dirtyEntries.length} uncommitted change(s)`;
97
+ }
98
+ return `Implementation completed without opening a PR; worktree still has ${dirtyEntries.length} uncommitted change(s)`;
99
+ }
100
+ }
101
+ catch {
102
+ // Best effort only.
103
+ }
104
+ try {
105
+ const ahead = await execCommand(this.config.runner.gitBin, [
106
+ "-C",
107
+ issue.worktreePath,
108
+ "rev-list",
109
+ "--count",
110
+ `origin/${baseBranch}..HEAD`,
111
+ ], { timeoutMs: 10_000 });
112
+ if (ahead.exitCode === 0) {
113
+ const count = Number(ahead.stdout.trim());
114
+ if (Number.isFinite(count) && count > 0) {
115
+ if (deliveryMode === "linear_only") {
116
+ return `Planning-only implementation should not create repo commits; worktree is ${count} local commit(s) ahead of origin/${baseBranch}`;
117
+ }
118
+ return `Implementation completed with ${count} local commit(s) ahead of origin/${baseBranch} but no PR was observed`;
119
+ }
120
+ }
121
+ }
122
+ catch {
123
+ // Best effort only.
124
+ }
125
+ return undefined;
126
+ }
127
+ }
@@ -0,0 +1,232 @@
1
+ import { parseGitHubFailureContext } from "./github-failure-context.js";
2
+ import { isIssueSessionReadyForExecution } from "./issue-session.js";
3
+ import { deriveIssueStatusNote } from "./status-note.js";
4
+ import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
5
+ export function parseStageReport(reportJson, runStatus) {
6
+ if (!reportJson)
7
+ return undefined;
8
+ try {
9
+ const parsed = JSON.parse(reportJson);
10
+ return { ...parsed, status: runStatus };
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ export class IssueOverviewQuery {
17
+ db;
18
+ codex;
19
+ runStatusProvider;
20
+ constructor(db, codex, runStatusProvider) {
21
+ this.db = db;
22
+ this.codex = codex;
23
+ this.runStatusProvider = runStatusProvider;
24
+ }
25
+ async getIssueOverview(issueKey) {
26
+ const session = this.db.issueSessions.getIssueSessionByKey(issueKey);
27
+ if (!session) {
28
+ return await this.getLegacyIssueOverview(issueKey);
29
+ }
30
+ return await this.getSessionIssueOverview(issueKey, session);
31
+ }
32
+ buildRuns(projectId, linearIssueId) {
33
+ return this.db.runs.listRunsForIssue(projectId, linearIssueId).map((run) => ({
34
+ id: run.id,
35
+ runType: run.runType,
36
+ status: run.status,
37
+ startedAt: run.startedAt,
38
+ ...(run.endedAt ? { endedAt: run.endedAt } : {}),
39
+ ...(run.threadId ? { threadId: run.threadId } : {}),
40
+ ...(() => {
41
+ const report = parseStageReport(run.reportJson, run.status);
42
+ return report ? { report } : {};
43
+ })(),
44
+ ...(() => {
45
+ const events = this.db.runs.listThreadEvents(run.id).flatMap((event) => {
46
+ try {
47
+ const parsed = JSON.parse(event.eventJson);
48
+ return [{
49
+ id: event.id,
50
+ method: event.method,
51
+ createdAt: event.createdAt,
52
+ ...(parsed && typeof parsed === "object" && !Array.isArray(parsed)
53
+ ? { parsedEvent: parsed }
54
+ : {}),
55
+ }];
56
+ }
57
+ catch {
58
+ return [{
59
+ id: event.id,
60
+ method: event.method,
61
+ createdAt: event.createdAt,
62
+ }];
63
+ }
64
+ });
65
+ return events.length > 0 ? { events } : {};
66
+ })(),
67
+ }));
68
+ }
69
+ async getLegacyIssueOverview(issueKey) {
70
+ const legacy = this.db.getIssueOverview(issueKey);
71
+ if (!legacy)
72
+ return undefined;
73
+ const issueRecord = this.db.issues.getIssueByKey(issueKey);
74
+ const activeStatus = await this.runStatusProvider.getActiveRunStatus(issueKey);
75
+ const activeRun = activeStatus?.run ?? legacy.activeRun;
76
+ const latestRun = this.db.runs.getLatestRunForIssue(legacy.issue.projectId, legacy.issue.linearIssueId);
77
+ const latestEvent = this.db.issueSessions.listIssueSessionEvents(legacy.issue.projectId, legacy.issue.linearIssueId, { limit: 1 }).at(-1);
78
+ const runs = this.buildRuns(legacy.issue.projectId, legacy.issue.linearIssueId);
79
+ const runCount = runs.length;
80
+ const liveThread = await this.readLiveThread(activeRun);
81
+ const statusNote = issueRecord
82
+ ? deriveIssueStatusNote({
83
+ issue: issueRecord,
84
+ latestRun,
85
+ latestEvent,
86
+ failureSummary: legacy.issue.latestFailureSummary,
87
+ blockedByKeys: legacy.issue.blockedByKeys,
88
+ waitingReason: legacy.issue.waitingReason,
89
+ })
90
+ : legacy.issue.statusNote;
91
+ return {
92
+ issue: {
93
+ ...legacy.issue,
94
+ ...(statusNote ? { statusNote } : {}),
95
+ },
96
+ ...(activeRun ? { activeRun } : {}),
97
+ ...(latestRun ? { latestRun } : {}),
98
+ ...(liveThread ? { liveThread } : {}),
99
+ ...(runs.length > 0 ? { runs } : {}),
100
+ ...(issueRecord
101
+ ? {
102
+ issueContext: {
103
+ ...(issueRecord.description ? { description: issueRecord.description } : {}),
104
+ ...(issueRecord.currentLinearState ? { currentLinearState: issueRecord.currentLinearState } : {}),
105
+ ...(issueRecord.url ? { issueUrl: issueRecord.url } : {}),
106
+ ...(issueRecord.worktreePath ? { worktreePath: issueRecord.worktreePath } : {}),
107
+ ...(issueRecord.branchName ? { branchName: issueRecord.branchName } : {}),
108
+ ...(issueRecord.prUrl ? { prUrl: issueRecord.prUrl } : {}),
109
+ ...(issueRecord.priority != null ? { priority: issueRecord.priority } : {}),
110
+ ...(issueRecord.estimate != null ? { estimate: issueRecord.estimate } : {}),
111
+ ciRepairAttempts: issueRecord.ciRepairAttempts,
112
+ queueRepairAttempts: issueRecord.queueRepairAttempts,
113
+ reviewFixAttempts: issueRecord.reviewFixAttempts,
114
+ ...(legacy.issue.latestFailureSource ? { latestFailureSource: legacy.issue.latestFailureSource } : {}),
115
+ ...(legacy.issue.latestFailureHeadSha ? { latestFailureHeadSha: legacy.issue.latestFailureHeadSha } : {}),
116
+ ...(legacy.issue.latestFailureCheckName ? { latestFailureCheckName: legacy.issue.latestFailureCheckName } : {}),
117
+ ...(legacy.issue.latestFailureStepName ? { latestFailureStepName: legacy.issue.latestFailureStepName } : {}),
118
+ ...(legacy.issue.latestFailureSummary ? { latestFailureSummary: legacy.issue.latestFailureSummary } : {}),
119
+ runCount,
120
+ },
121
+ }
122
+ : {}),
123
+ };
124
+ }
125
+ async getSessionIssueOverview(issueKey, session) {
126
+ const issueRecord = this.db.issues.getIssueByKey(issueKey);
127
+ const blockedBy = this.db.issues.listIssueDependencies(session.projectId, session.linearIssueId);
128
+ const unresolvedBlockedBy = blockedBy.filter((entry) => (entry.blockerCurrentLinearStateType !== "completed"
129
+ && entry.blockerCurrentLinearState?.trim().toLowerCase() !== "done"));
130
+ const blockedByKeys = unresolvedBlockedBy.map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId);
131
+ const activeStatus = await this.runStatusProvider.getActiveRunStatus(issueKey);
132
+ const activeRun = activeStatus?.run
133
+ ?? (session.activeRunId !== undefined ? this.db.runs.getRunById(session.activeRunId) : undefined);
134
+ const latestRun = this.db.runs.getLatestRunForIssue(session.projectId, session.linearIssueId);
135
+ const latestEvent = this.db.issueSessions.listIssueSessionEvents(session.projectId, session.linearIssueId, { limit: 1 }).at(-1);
136
+ const runs = this.buildRuns(session.projectId, session.linearIssueId);
137
+ const runCount = runs.length;
138
+ const liveThread = await this.readLiveThread(activeRun);
139
+ const failureContext = parseGitHubFailureContext(issueRecord?.lastGitHubFailureContextJson);
140
+ const waitingReason = session.waitingReason ?? derivePatchRelayWaitingReason({
141
+ ...(activeRun ? { activeRunType: activeRun.runType } : {}),
142
+ blockedByKeys,
143
+ factoryState: issueRecord?.factoryState ?? "delegated",
144
+ pendingRunType: issueRecord?.pendingRunType,
145
+ prNumber: session.prNumber,
146
+ prHeadSha: issueRecord?.prHeadSha ?? session.prHeadSha,
147
+ prReviewState: issueRecord?.prReviewState,
148
+ prCheckStatus: issueRecord?.prCheckStatus,
149
+ lastBlockingReviewHeadSha: issueRecord?.lastBlockingReviewHeadSha,
150
+ latestFailureCheckName: issueRecord?.lastGitHubFailureCheckName,
151
+ });
152
+ const issue = {
153
+ id: issueRecord?.id ?? session.id,
154
+ projectId: session.projectId,
155
+ linearIssueId: session.linearIssueId,
156
+ ...(session.issueKey ? { issueKey: session.issueKey } : {}),
157
+ ...(issueRecord?.title ? { title: issueRecord.title } : {}),
158
+ ...(issueRecord?.url ? { issueUrl: issueRecord.url } : {}),
159
+ ...(issueRecord?.currentLinearState ? { currentLinearState: issueRecord.currentLinearState } : {}),
160
+ sessionState: session.sessionState,
161
+ factoryState: issueRecord?.factoryState ?? "delegated",
162
+ blockedByCount: unresolvedBlockedBy.length,
163
+ blockedByKeys,
164
+ readyForExecution: isIssueSessionReadyForExecution({
165
+ sessionState: session.sessionState,
166
+ factoryState: issueRecord?.factoryState ?? "delegated",
167
+ ...(activeRun ? { activeRunId: activeRun.id } : {}),
168
+ blockedByCount: unresolvedBlockedBy.length,
169
+ hasPendingWake: this.db.issueSessions.peekIssueSessionWake(session.projectId, session.linearIssueId) !== undefined,
170
+ hasLegacyPendingRun: issueRecord?.pendingRunType !== undefined,
171
+ ...(session.prNumber !== undefined ? { prNumber: session.prNumber } : {}),
172
+ ...(issueRecord?.prState ? { prState: issueRecord.prState } : {}),
173
+ ...(issueRecord?.prReviewState ? { prReviewState: issueRecord.prReviewState } : {}),
174
+ ...(issueRecord?.prCheckStatus ? { prCheckStatus: issueRecord.prCheckStatus } : {}),
175
+ ...(issueRecord?.lastGitHubFailureSource ? { latestFailureSource: issueRecord.lastGitHubFailureSource } : {}),
176
+ }),
177
+ ...(issueRecord?.lastGitHubFailureSource ? { latestFailureSource: issueRecord.lastGitHubFailureSource } : {}),
178
+ ...(issueRecord?.lastGitHubFailureHeadSha ? { latestFailureHeadSha: issueRecord.lastGitHubFailureHeadSha } : {}),
179
+ ...(issueRecord?.lastGitHubFailureCheckName ? { latestFailureCheckName: issueRecord.lastGitHubFailureCheckName } : {}),
180
+ ...(() => {
181
+ const statusNote = issueRecord
182
+ ? deriveIssueStatusNote({
183
+ issue: issueRecord,
184
+ sessionSummary: session.summaryText,
185
+ latestRun,
186
+ latestEvent,
187
+ failureSummary: failureContext?.summary,
188
+ blockedByKeys,
189
+ waitingReason,
190
+ })
191
+ : undefined;
192
+ return statusNote ? { statusNote } : {};
193
+ })(),
194
+ ...(waitingReason ? { waitingReason } : {}),
195
+ ...(activeRun ? { activeRunId: activeRun.id } : {}),
196
+ ...(issueRecord?.agentSessionId ? { activeAgentSessionId: issueRecord.agentSessionId } : {}),
197
+ updatedAt: session.updatedAt,
198
+ };
199
+ return {
200
+ issue,
201
+ session,
202
+ ...(activeRun ? { activeRun } : {}),
203
+ ...(latestRun ? { latestRun } : {}),
204
+ ...(liveThread ? { liveThread } : {}),
205
+ ...(runs.length > 0 ? { runs } : {}),
206
+ issueContext: {
207
+ ...(issueRecord?.description ? { description: issueRecord.description } : {}),
208
+ ...(issueRecord?.currentLinearState ? { currentLinearState: issueRecord.currentLinearState } : {}),
209
+ ...(issueRecord?.url ? { issueUrl: issueRecord.url } : {}),
210
+ ...(session.worktreePath ? { worktreePath: session.worktreePath } : {}),
211
+ ...(session.branchName ? { branchName: session.branchName } : {}),
212
+ ...(issueRecord?.prUrl ? { prUrl: issueRecord.prUrl } : {}),
213
+ ...(issueRecord?.priority != null ? { priority: issueRecord.priority } : {}),
214
+ ...(issueRecord?.estimate != null ? { estimate: issueRecord.estimate } : {}),
215
+ ciRepairAttempts: issueRecord?.ciRepairAttempts ?? session.ciRepairAttempts,
216
+ queueRepairAttempts: issueRecord?.queueRepairAttempts ?? session.queueRepairAttempts,
217
+ reviewFixAttempts: issueRecord?.reviewFixAttempts ?? session.reviewFixAttempts,
218
+ ...(issue.latestFailureSource ? { latestFailureSource: issue.latestFailureSource } : {}),
219
+ ...(issue.latestFailureHeadSha ? { latestFailureHeadSha: issue.latestFailureHeadSha } : {}),
220
+ ...(issue.latestFailureCheckName ? { latestFailureCheckName: issue.latestFailureCheckName } : {}),
221
+ ...(issue.latestFailureStepName ? { latestFailureStepName: issue.latestFailureStepName } : {}),
222
+ ...(issue.latestFailureSummary ? { latestFailureSummary: issue.latestFailureSummary } : {}),
223
+ runCount,
224
+ },
225
+ };
226
+ }
227
+ async readLiveThread(run) {
228
+ if (!run?.threadId)
229
+ return undefined;
230
+ return await this.codex.readThread(run.threadId, true).catch(() => undefined);
231
+ }
232
+ }