patchrelay 0.35.11 → 0.35.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.
Files changed (52) hide show
  1. package/README.md +41 -9
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli/args.js +19 -1
  4. package/dist/cli/commands/issues.js +18 -56
  5. package/dist/cli/commands/watch.js +5 -0
  6. package/dist/cli/data.js +160 -47
  7. package/dist/cli/formatters/text.js +51 -90
  8. package/dist/cli/help.js +15 -8
  9. package/dist/cli/index.js +3 -58
  10. package/dist/cli/operator-client.js +0 -82
  11. package/dist/cli/watch/App.js +21 -12
  12. package/dist/cli/watch/HelpBar.js +3 -3
  13. package/dist/cli/watch/IssueDetailView.js +63 -130
  14. package/dist/cli/watch/IssueRow.js +82 -27
  15. package/dist/cli/watch/StatusBar.js +8 -4
  16. package/dist/cli/watch/detail-rows.js +589 -0
  17. package/dist/cli/watch/render-rich-text.js +226 -0
  18. package/dist/cli/watch/state-visualization.js +48 -23
  19. package/dist/cli/watch/timeline-builder.js +2 -1
  20. package/dist/cli/watch/use-detail-stream.js +10 -104
  21. package/dist/cli/watch/use-watch-stream.js +11 -102
  22. package/dist/cli/watch/watch-state.js +129 -56
  23. package/dist/codex-thread-utils.js +3 -0
  24. package/dist/db/migrations.js +239 -2
  25. package/dist/db.js +628 -39
  26. package/dist/github-app-token.js +7 -0
  27. package/dist/github-failure-context.js +44 -1
  28. package/dist/github-rollup.js +47 -0
  29. package/dist/github-webhook-handler.js +423 -52
  30. package/dist/github-webhooks.js +7 -0
  31. package/dist/http.js +12 -264
  32. package/dist/idle-reconciliation.js +268 -76
  33. package/dist/issue-query-service.js +221 -129
  34. package/dist/issue-session-events.js +151 -0
  35. package/dist/issue-session.js +99 -0
  36. package/dist/linear-client.js +39 -25
  37. package/dist/linear-session-reporting.js +12 -0
  38. package/dist/linear-session-sync.js +253 -24
  39. package/dist/linear-workflow.js +33 -0
  40. package/dist/merge-queue-protocol.js +0 -51
  41. package/dist/preflight.js +1 -4
  42. package/dist/queue-health-monitor.js +11 -7
  43. package/dist/run-orchestrator.js +1364 -147
  44. package/dist/run-reporting.js +5 -3
  45. package/dist/service.js +279 -102
  46. package/dist/status-note.js +56 -0
  47. package/dist/waiting-reason.js +65 -0
  48. package/dist/webhook-handler.js +270 -79
  49. package/package.json +3 -2
  50. package/dist/cli/commands/feed.js +0 -60
  51. package/dist/cli/watch/FeedView.js +0 -28
  52. package/dist/cli/watch/use-feed-stream.js +0 -92
@@ -1,115 +1,225 @@
1
- import { parseGitHubFailureContext, summarizeGitHubFailureContext } from "./github-failure-context.js";
2
- import { parseStoredQueueRepairContext } from "./merge-queue-incident.js";
3
- import { resolveMergeQueueProtocol } from "./merge-queue-protocol.js";
1
+ import { parseGitHubFailureContext } from "./github-failure-context.js";
2
+ import { isIssueSessionReadyForExecution } from "./issue-session.js";
4
3
  import { extractStageSummary, summarizeCurrentThread } from "./run-reporting.js";
5
- import { safeJsonParse } from "./utils.js";
4
+ import { deriveIssueStatusNote } from "./status-note.js";
5
+ import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
6
+ function parseStageReport(reportJson, runStatus) {
7
+ if (!reportJson)
8
+ return undefined;
9
+ try {
10
+ const parsed = JSON.parse(reportJson);
11
+ return { ...parsed, status: runStatus };
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
6
17
  export class IssueQueryService {
7
- config;
8
18
  db;
9
19
  codex;
10
20
  runStatusProvider;
11
- constructor(config, db, codex, runStatusProvider) {
12
- this.config = config;
21
+ constructor(db, codex, runStatusProvider) {
13
22
  this.db = db;
14
23
  this.codex = codex;
15
24
  this.runStatusProvider = runStatusProvider;
16
25
  }
17
- async getIssueOverview(issueKey) {
18
- const result = this.db.getIssueOverview(issueKey);
19
- if (!result)
26
+ async readLiveThread(run) {
27
+ if (!run?.threadId)
20
28
  return undefined;
21
- const issueRecord = this.db.getIssueByKey(issueKey);
22
- const activeStatus = await this.runStatusProvider.getActiveRunStatus(issueKey);
23
- const activeRun = activeStatus?.run ?? result.activeRun;
24
- const latestRun = this.db.getLatestRunForIssue(result.issue.projectId, result.issue.linearIssueId);
25
- let liveThread;
26
- if (activeStatus?.liveThread) {
27
- liveThread = activeStatus.liveThread;
28
- }
29
- else if (activeRun?.threadId) {
30
- liveThread = await this.codex.readThread(activeRun.threadId, true).then(summarizeCurrentThread).catch(() => undefined);
31
- }
32
- return {
33
- ...result,
34
- issue: issueRecord ? { ...result.issue, queueProtocol: this.buildQueueProtocol(issueRecord.projectId, issueRecord) } : result.issue,
35
- ...(activeRun ? { activeRun } : {}),
36
- ...(latestRun ? { latestRun } : {}),
37
- ...(liveThread ? { liveThread } : {}),
38
- };
39
- }
40
- async getIssueReport(issueKey) {
41
- const issue = this.db.getTrackedIssueByKey(issueKey);
42
- if (!issue)
43
- return undefined;
44
- return {
45
- issue,
46
- runs: this.db.listRunsForIssue(issue.projectId, issue.linearIssueId).map((run) => ({
47
- run,
48
- ...(run.reportJson ? { report: JSON.parse(run.reportJson) } : {}),
49
- })),
50
- };
51
- }
52
- async getRunEvents(issueKey, runId) {
53
- const issue = this.db.getTrackedIssueByKey(issueKey);
54
- if (!issue)
55
- return undefined;
56
- const run = this.db.getRun(runId);
57
- if (!run || run.projectId !== issue.projectId || run.linearIssueId !== issue.linearIssueId)
58
- return undefined;
59
- return {
60
- issue,
61
- run,
62
- events: this.db.listThreadEvents(runId).map((event) => ({
63
- ...event,
64
- parsedEvent: safeJsonParse(event.eventJson),
65
- })),
66
- };
29
+ return await this.codex.readThread(run.threadId, true).catch(() => undefined);
67
30
  }
68
- async getIssueTimeline(issueKey) {
69
- const issue = this.db.getTrackedIssueByKey(issueKey);
70
- if (!issue)
71
- return undefined;
72
- const fullIssue = this.db.getIssueByKey(issueKey);
73
- const runs = this.db.listRunsForIssue(issue.projectId, issue.linearIssueId).map((run) => ({
31
+ buildRuns(projectId, linearIssueId) {
32
+ return this.db.listRunsForIssue(projectId, linearIssueId).map((run) => ({
74
33
  id: run.id,
75
34
  runType: run.runType,
76
35
  status: run.status,
77
36
  startedAt: run.startedAt,
78
- endedAt: run.endedAt,
79
- threadId: run.threadId,
80
- events: this.db.listThreadEvents(run.id).map((event) => ({
81
- id: event.id,
82
- method: event.method,
83
- createdAt: event.createdAt,
84
- parsedEvent: safeJsonParse(event.eventJson),
85
- })),
86
- ...(run.reportJson ? { report: JSON.parse(run.reportJson) } : {}),
37
+ ...(run.endedAt ? { endedAt: run.endedAt } : {}),
38
+ ...(run.threadId ? { threadId: run.threadId } : {}),
39
+ ...(() => {
40
+ const report = parseStageReport(run.reportJson, run.status);
41
+ return report ? { report } : {};
42
+ })(),
43
+ ...(() => {
44
+ const events = this.db.listThreadEvents(run.id).flatMap((event) => {
45
+ try {
46
+ const parsed = JSON.parse(event.eventJson);
47
+ return [{
48
+ id: event.id,
49
+ method: event.method,
50
+ createdAt: event.createdAt,
51
+ ...(parsed && typeof parsed === "object" && !Array.isArray(parsed)
52
+ ? { parsedEvent: parsed }
53
+ : {}),
54
+ }];
55
+ }
56
+ catch {
57
+ return [{
58
+ id: event.id,
59
+ method: event.method,
60
+ createdAt: event.createdAt,
61
+ }];
62
+ }
63
+ });
64
+ return events.length > 0 ? { events } : {};
65
+ })(),
87
66
  }));
88
- const feedEvents = this.db.operatorFeed.list({ issueKey, limit: 500 });
89
- let liveThread = undefined;
90
- const activeRunId = fullIssue?.activeRunId;
91
- const activeRun = activeRunId !== undefined ? runs.find((r) => r.id === activeRunId) : undefined;
92
- if (activeRun?.threadId) {
93
- liveThread = await this.codex.readThread(activeRun.threadId, true).catch(() => undefined);
67
+ }
68
+ async getIssueOverview(issueKey) {
69
+ const session = this.db.getIssueSessionByKey(issueKey);
70
+ if (!session) {
71
+ const legacy = this.db.getIssueOverview(issueKey);
72
+ if (!legacy)
73
+ return undefined;
74
+ const issueRecord = this.db.getIssueByKey(issueKey);
75
+ const activeStatus = await this.runStatusProvider.getActiveRunStatus(issueKey);
76
+ const activeRun = activeStatus?.run ?? legacy.activeRun;
77
+ const latestRun = this.db.getLatestRunForIssue(legacy.issue.projectId, legacy.issue.linearIssueId);
78
+ const latestEvent = this.db.listIssueSessionEvents(legacy.issue.projectId, legacy.issue.linearIssueId, { limit: 1 }).at(-1);
79
+ const runs = this.buildRuns(legacy.issue.projectId, legacy.issue.linearIssueId);
80
+ const runCount = runs.length;
81
+ const liveThread = await this.readLiveThread(activeRun);
82
+ const statusNote = issueRecord
83
+ ? deriveIssueStatusNote({
84
+ issue: issueRecord,
85
+ latestRun,
86
+ latestEvent,
87
+ failureSummary: legacy.issue.latestFailureSummary,
88
+ blockedByKeys: legacy.issue.blockedByKeys,
89
+ waitingReason: legacy.issue.waitingReason,
90
+ })
91
+ : legacy.issue.statusNote;
92
+ return {
93
+ issue: {
94
+ ...legacy.issue,
95
+ ...(statusNote ? { statusNote } : {}),
96
+ },
97
+ ...(activeRun ? { activeRun } : {}),
98
+ ...(latestRun ? { latestRun } : {}),
99
+ ...(liveThread ? { liveThread } : {}),
100
+ ...(runs.length > 0 ? { runs } : {}),
101
+ ...(issueRecord
102
+ ? {
103
+ issueContext: {
104
+ ...(issueRecord.description ? { description: issueRecord.description } : {}),
105
+ ...(issueRecord.currentLinearState ? { currentLinearState: issueRecord.currentLinearState } : {}),
106
+ ...(issueRecord.url ? { issueUrl: issueRecord.url } : {}),
107
+ ...(issueRecord.worktreePath ? { worktreePath: issueRecord.worktreePath } : {}),
108
+ ...(issueRecord.branchName ? { branchName: issueRecord.branchName } : {}),
109
+ ...(issueRecord.prUrl ? { prUrl: issueRecord.prUrl } : {}),
110
+ ...(issueRecord.priority != null ? { priority: issueRecord.priority } : {}),
111
+ ...(issueRecord.estimate != null ? { estimate: issueRecord.estimate } : {}),
112
+ ciRepairAttempts: issueRecord.ciRepairAttempts,
113
+ queueRepairAttempts: issueRecord.queueRepairAttempts,
114
+ reviewFixAttempts: issueRecord.reviewFixAttempts,
115
+ ...(legacy.issue.latestFailureSource ? { latestFailureSource: legacy.issue.latestFailureSource } : {}),
116
+ ...(legacy.issue.latestFailureHeadSha ? { latestFailureHeadSha: legacy.issue.latestFailureHeadSha } : {}),
117
+ ...(legacy.issue.latestFailureCheckName ? { latestFailureCheckName: legacy.issue.latestFailureCheckName } : {}),
118
+ ...(legacy.issue.latestFailureStepName ? { latestFailureStepName: legacy.issue.latestFailureStepName } : {}),
119
+ ...(legacy.issue.latestFailureSummary ? { latestFailureSummary: legacy.issue.latestFailureSummary } : {}),
120
+ runCount,
121
+ },
122
+ }
123
+ : {}),
124
+ };
94
125
  }
126
+ const issueRecord = this.db.getIssueByKey(issueKey);
127
+ const blockedBy = this.db.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.getRun(session.activeRunId) : undefined);
134
+ const latestRun = this.db.getLatestRunForIssue(session.projectId, session.linearIssueId);
135
+ const latestEvent = this.db.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
+ prReviewState: issueRecord?.prReviewState,
147
+ prCheckStatus: issueRecord?.prCheckStatus,
148
+ latestFailureCheckName: issueRecord?.lastGitHubFailureCheckName,
149
+ });
150
+ const issue = {
151
+ id: issueRecord?.id ?? session.id,
152
+ projectId: session.projectId,
153
+ linearIssueId: session.linearIssueId,
154
+ ...(session.issueKey ? { issueKey: session.issueKey } : {}),
155
+ ...(issueRecord?.title ? { title: issueRecord.title } : {}),
156
+ ...(issueRecord?.url ? { issueUrl: issueRecord.url } : {}),
157
+ ...(issueRecord?.currentLinearState ? { currentLinearState: issueRecord.currentLinearState } : {}),
158
+ sessionState: session.sessionState,
159
+ factoryState: issueRecord?.factoryState ?? "delegated",
160
+ blockedByCount: unresolvedBlockedBy.length,
161
+ blockedByKeys,
162
+ readyForExecution: isIssueSessionReadyForExecution({
163
+ sessionState: session.sessionState,
164
+ factoryState: issueRecord?.factoryState ?? "delegated",
165
+ ...(activeRun ? { activeRunId: activeRun.id } : {}),
166
+ blockedByCount: unresolvedBlockedBy.length,
167
+ hasPendingWake: this.db.peekIssueSessionWake(session.projectId, session.linearIssueId) !== undefined,
168
+ hasLegacyPendingRun: issueRecord?.pendingRunType !== undefined,
169
+ ...(session.prNumber !== undefined ? { prNumber: session.prNumber } : {}),
170
+ ...(issueRecord?.prState ? { prState: issueRecord.prState } : {}),
171
+ ...(issueRecord?.prReviewState ? { prReviewState: issueRecord.prReviewState } : {}),
172
+ ...(issueRecord?.prCheckStatus ? { prCheckStatus: issueRecord.prCheckStatus } : {}),
173
+ ...(issueRecord?.lastGitHubFailureSource ? { latestFailureSource: issueRecord.lastGitHubFailureSource } : {}),
174
+ }),
175
+ ...(issueRecord?.lastGitHubFailureSource ? { latestFailureSource: issueRecord.lastGitHubFailureSource } : {}),
176
+ ...(issueRecord?.lastGitHubFailureHeadSha ? { latestFailureHeadSha: issueRecord.lastGitHubFailureHeadSha } : {}),
177
+ ...(issueRecord?.lastGitHubFailureCheckName ? { latestFailureCheckName: issueRecord.lastGitHubFailureCheckName } : {}),
178
+ ...(() => {
179
+ const statusNote = issueRecord
180
+ ? deriveIssueStatusNote({
181
+ issue: issueRecord,
182
+ sessionSummary: session.summaryText,
183
+ latestRun,
184
+ latestEvent,
185
+ failureSummary: failureContext?.summary,
186
+ blockedByKeys,
187
+ waitingReason,
188
+ })
189
+ : undefined;
190
+ return statusNote ? { statusNote } : {};
191
+ })(),
192
+ ...(waitingReason ? { waitingReason } : {}),
193
+ ...(activeRun ? { activeRunId: activeRun.id } : {}),
194
+ ...(issueRecord?.agentSessionId ? { activeAgentSessionId: issueRecord.agentSessionId } : {}),
195
+ updatedAt: session.updatedAt,
196
+ };
95
197
  return {
96
- issue: {
97
- ...issue,
98
- ...(fullIssue?.description ? { description: fullIssue.description } : {}),
99
- ...(fullIssue?.branchName ? { branchName: fullIssue.branchName } : {}),
100
- ...(fullIssue?.worktreePath ? { worktreePath: fullIssue.worktreePath } : {}),
101
- ...(fullIssue?.prUrl ? { prUrl: fullIssue.prUrl } : {}),
102
- ...(fullIssue?.priority != null ? { priority: fullIssue.priority } : {}),
103
- ...(fullIssue?.estimate != null ? { estimate: fullIssue.estimate } : {}),
104
- ciRepairAttempts: fullIssue?.ciRepairAttempts ?? 0,
105
- queueRepairAttempts: fullIssue?.queueRepairAttempts ?? 0,
106
- reviewFixAttempts: fullIssue?.reviewFixAttempts ?? 0,
107
- ...(fullIssue ? { queueProtocol: this.buildQueueProtocol(fullIssue.projectId, fullIssue) } : {}),
198
+ issue,
199
+ session,
200
+ ...(activeRun ? { activeRun } : {}),
201
+ ...(latestRun ? { latestRun } : {}),
202
+ ...(liveThread ? { liveThread } : {}),
203
+ ...(runs.length > 0 ? { runs } : {}),
204
+ issueContext: {
205
+ ...(issueRecord?.description ? { description: issueRecord.description } : {}),
206
+ ...(issueRecord?.currentLinearState ? { currentLinearState: issueRecord.currentLinearState } : {}),
207
+ ...(issueRecord?.url ? { issueUrl: issueRecord.url } : {}),
208
+ ...(session.worktreePath ? { worktreePath: session.worktreePath } : {}),
209
+ ...(session.branchName ? { branchName: session.branchName } : {}),
210
+ ...(issueRecord?.prUrl ? { prUrl: issueRecord.prUrl } : {}),
211
+ ...(issueRecord?.priority != null ? { priority: issueRecord.priority } : {}),
212
+ ...(issueRecord?.estimate != null ? { estimate: issueRecord.estimate } : {}),
213
+ ciRepairAttempts: issueRecord?.ciRepairAttempts ?? session.ciRepairAttempts,
214
+ queueRepairAttempts: issueRecord?.queueRepairAttempts ?? session.queueRepairAttempts,
215
+ reviewFixAttempts: issueRecord?.reviewFixAttempts ?? session.reviewFixAttempts,
216
+ ...(issue.latestFailureSource ? { latestFailureSource: issue.latestFailureSource } : {}),
217
+ ...(issue.latestFailureHeadSha ? { latestFailureHeadSha: issue.latestFailureHeadSha } : {}),
218
+ ...(issue.latestFailureCheckName ? { latestFailureCheckName: issue.latestFailureCheckName } : {}),
219
+ ...(issue.latestFailureStepName ? { latestFailureStepName: issue.latestFailureStepName } : {}),
220
+ ...(issue.latestFailureSummary ? { latestFailureSummary: issue.latestFailureSummary } : {}),
221
+ runCount,
108
222
  },
109
- runs,
110
- feedEvents,
111
- liveThread,
112
- activeRunId,
113
223
  };
114
224
  }
115
225
  async getActiveRunStatus(issueKey) {
@@ -120,59 +230,41 @@ export class IssueQueryService {
120
230
  if (!overview)
121
231
  return undefined;
122
232
  const issueRecord = this.db.getIssueByKey(issueKey);
123
- const report = await this.getIssueReport(issueKey);
124
- const latestRunReport = report?.runs.at(-1)?.report;
233
+ const latestRunReport = parseStageReport(overview.latestRun?.reportJson, overview.latestRun?.status ?? "unknown");
234
+ const runs = (overview.runs ?? this.buildRuns(overview.issue.projectId, overview.issue.linearIssueId)).map((run) => ({
235
+ run: {
236
+ id: run.id,
237
+ runType: run.runType,
238
+ status: run.status,
239
+ startedAt: run.startedAt,
240
+ ...(run.endedAt ? { endedAt: run.endedAt } : {}),
241
+ },
242
+ ...(run.report ? { report: run.report } : {}),
243
+ }));
125
244
  return {
126
245
  issue: {
127
246
  issueKey: overview.issue.issueKey,
128
247
  title: overview.issue.title,
129
248
  issueUrl: overview.issue.issueUrl,
130
249
  currentLinearState: overview.issue.currentLinearState,
250
+ ...(overview.session?.sessionState ? { sessionState: overview.session.sessionState } : {}),
131
251
  factoryState: overview.issue.factoryState,
132
- ...(issueRecord?.prNumber !== undefined ? { prNumber: issueRecord.prNumber } : {}),
252
+ ...(overview.session?.prNumber !== undefined ? { prNumber: overview.session.prNumber } : {}),
133
253
  ...(issueRecord?.prUrl ? { prUrl: issueRecord.prUrl } : {}),
134
254
  ...(issueRecord?.prState ? { prState: issueRecord.prState } : {}),
135
255
  ...(issueRecord?.prReviewState ? { prReviewState: issueRecord.prReviewState } : {}),
136
256
  ...(issueRecord?.prCheckStatus ? { prCheckStatus: issueRecord.prCheckStatus } : {}),
137
257
  ...(issueRecord ? { ciRepairAttempts: issueRecord.ciRepairAttempts, queueRepairAttempts: issueRecord.queueRepairAttempts } : {}),
138
- ...(issueRecord ? { queueProtocol: this.buildQueueProtocol(issueRecord.projectId, issueRecord) } : {}),
258
+ ...(overview.issue.waitingReason ? { waitingReason: overview.issue.waitingReason } : {}),
259
+ ...(overview.issue.statusNote ? { statusNote: overview.issue.statusNote } : {}),
260
+ ...(overview.session?.lastWakeReason ? { lastWakeReason: overview.session.lastWakeReason } : {}),
139
261
  },
140
262
  ...(overview.activeRun ? { activeRun: overview.activeRun } : {}),
141
263
  ...(overview.latestRun ? { latestRun: overview.latestRun } : {}),
142
- ...(overview.liveThread ? { liveThread: overview.liveThread } : {}),
264
+ ...(overview.liveThread ? { liveThread: summarizeCurrentThread(overview.liveThread) } : {}),
143
265
  ...(latestRunReport ? { latestReportSummary: extractStageSummary(latestRunReport) } : {}),
144
- feedEvents: this.db.operatorFeed.list({ issueKey, limit: 500 }),
145
- activeRunId: issueRecord?.activeRunId ?? null,
146
- runs: report?.runs ?? [],
266
+ runs,
147
267
  generatedAt: new Date().toISOString(),
148
268
  };
149
269
  }
150
- buildQueueProtocol(projectId, issue) {
151
- const project = this.config.projects.find((entry) => entry.id === projectId);
152
- const protocol = resolveMergeQueueProtocol(project);
153
- const failureContext = parseGitHubFailureContext(issue.lastGitHubFailureContextJson);
154
- const queueIncident = issue.lastQueueIncidentJson
155
- ? parseStoredQueueRepairContext(issue.lastQueueIncidentJson)
156
- : undefined;
157
- return {
158
- repoFullName: protocol.repoFullName,
159
- baseBranch: protocol.baseBranch,
160
- admissionLabel: protocol.admissionLabel,
161
- evictionCheckName: protocol.evictionCheckName,
162
- prNumber: issue.prNumber ?? null,
163
- lastFailureSource: issue.lastGitHubFailureSource ?? null,
164
- lastFailureHeadSha: issue.lastGitHubFailureHeadSha ?? failureContext?.headSha ?? null,
165
- lastFailureSignature: issue.lastGitHubFailureSignature ?? failureContext?.failureSignature ?? null,
166
- lastFailureCheckName: issue.lastGitHubFailureCheckName ?? null,
167
- lastFailureCheckUrl: issue.lastGitHubFailureCheckUrl ?? null,
168
- lastFailureStepName: failureContext?.stepName ?? null,
169
- lastFailureSummary: summarizeGitHubFailureContext(failureContext) ?? null,
170
- lastFailureAt: issue.lastGitHubFailureAt ?? null,
171
- lastQueueSignalAt: issue.lastQueueSignalAt ?? null,
172
- lastIncidentId: queueIncident?.incidentId ?? null,
173
- lastIncidentUrl: queueIncident?.incidentUrl ?? null,
174
- lastIncidentFailureClass: queueIncident?.incidentContext?.failureClass ?? null,
175
- lastIncidentSummary: queueIncident?.incidentSummary ?? null,
176
- };
177
- }
178
270
  }
@@ -0,0 +1,151 @@
1
+ const TERMINAL_SESSION_EVENTS = new Set([
2
+ "stop_requested",
3
+ "undelegated",
4
+ "issue_removed",
5
+ "pr_closed",
6
+ "pr_merged",
7
+ ]);
8
+ export function deriveSessionWakePlan(issue, events) {
9
+ if (events.length === 0)
10
+ return undefined;
11
+ if (events.some((event) => TERMINAL_SESSION_EVENTS.has(event.eventType))) {
12
+ return undefined;
13
+ }
14
+ const context = {};
15
+ const followUps = [];
16
+ let wakeReason;
17
+ let runType;
18
+ let resumeThread = false;
19
+ for (const event of events) {
20
+ const payload = parseEventJson(event.eventJson);
21
+ switch (event.eventType) {
22
+ case "merge_steward_incident":
23
+ runType = "queue_repair";
24
+ wakeReason = "merge_steward_incident";
25
+ Object.assign(context, payload ?? {});
26
+ break;
27
+ case "settled_red_ci":
28
+ if (runType !== "queue_repair") {
29
+ runType = "ci_repair";
30
+ wakeReason = "settled_red_ci";
31
+ Object.assign(context, payload ?? {});
32
+ }
33
+ break;
34
+ case "review_changes_requested":
35
+ if (runType !== "queue_repair" && runType !== "ci_repair") {
36
+ runType = "review_fix";
37
+ wakeReason = "review_changes_requested";
38
+ Object.assign(context, payload ?? {});
39
+ }
40
+ break;
41
+ case "delegated":
42
+ if (!runType) {
43
+ runType = "implementation";
44
+ wakeReason = "delegated";
45
+ }
46
+ if (payload?.promptContext !== undefined) {
47
+ context.promptContext = payload.promptContext;
48
+ }
49
+ if (payload?.promptBody !== undefined) {
50
+ context.promptBody = payload.promptBody;
51
+ }
52
+ break;
53
+ case "direct_reply": {
54
+ if (!runType) {
55
+ runType = issue.prReviewState === "changes_requested" ? "review_fix" : "implementation";
56
+ wakeReason = "direct_reply";
57
+ }
58
+ const text = typeof payload?.text === "string"
59
+ ? payload.text
60
+ : typeof payload?.body === "string" ? payload.body : undefined;
61
+ if (text) {
62
+ followUps.push({
63
+ type: event.eventType,
64
+ text,
65
+ ...(typeof payload?.author === "string" ? { author: payload.author } : {}),
66
+ });
67
+ }
68
+ context.directReplyMode = true;
69
+ resumeThread = true;
70
+ break;
71
+ }
72
+ case "followup_prompt":
73
+ case "followup_comment":
74
+ case "operator_prompt": {
75
+ if (!runType) {
76
+ runType = issue.prReviewState === "changes_requested" ? "review_fix" : "implementation";
77
+ wakeReason = event.eventType;
78
+ }
79
+ const text = typeof payload?.text === "string"
80
+ ? payload.text
81
+ : typeof payload?.body === "string" ? payload.body : undefined;
82
+ if (text) {
83
+ followUps.push({
84
+ type: event.eventType,
85
+ text,
86
+ ...(typeof payload?.author === "string" ? { author: payload.author } : {}),
87
+ });
88
+ }
89
+ if (event.eventType === "followup_prompt"
90
+ || event.eventType === "followup_comment"
91
+ || event.eventType === "operator_prompt") {
92
+ resumeThread = true;
93
+ }
94
+ break;
95
+ }
96
+ default:
97
+ break;
98
+ }
99
+ }
100
+ if (!runType)
101
+ return undefined;
102
+ if (followUps.length > 0) {
103
+ context.followUps = followUps;
104
+ context.followUpMode = true;
105
+ context.followUpCount = followUps.length;
106
+ }
107
+ if (wakeReason) {
108
+ context.wakeReason = wakeReason;
109
+ }
110
+ return { runType, wakeReason, resumeThread, context };
111
+ }
112
+ export function extractLatestAssistantSummary(run) {
113
+ if (!run)
114
+ return undefined;
115
+ if (run.summaryJson) {
116
+ try {
117
+ const parsed = JSON.parse(run.summaryJson);
118
+ if (typeof parsed.latestAssistantMessage === "string" && parsed.latestAssistantMessage.trim()) {
119
+ return parsed.latestAssistantMessage;
120
+ }
121
+ }
122
+ catch {
123
+ // ignore malformed summary json
124
+ }
125
+ }
126
+ if (run.reportJson) {
127
+ try {
128
+ const parsed = JSON.parse(run.reportJson);
129
+ if (Array.isArray(parsed.assistantMessages)) {
130
+ const latest = parsed.assistantMessages.findLast((value) => typeof value === "string" && value.trim());
131
+ if (typeof latest === "string")
132
+ return latest;
133
+ }
134
+ }
135
+ catch {
136
+ // ignore malformed report json
137
+ }
138
+ }
139
+ return run.failureReason;
140
+ }
141
+ function parseEventJson(raw) {
142
+ if (!raw)
143
+ return undefined;
144
+ try {
145
+ const parsed = JSON.parse(raw);
146
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
147
+ }
148
+ catch {
149
+ return undefined;
150
+ }
151
+ }
@@ -0,0 +1,99 @@
1
+ import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
2
+ export function deriveIssueSessionState(params) {
3
+ if (params.factoryState === "done")
4
+ return "done";
5
+ if (params.factoryState === "failed" || params.factoryState === "escalated")
6
+ return "failed";
7
+ if (params.factoryState === "awaiting_input")
8
+ return "waiting_input";
9
+ if (params.activeRunId !== undefined)
10
+ return "running";
11
+ return "idle";
12
+ }
13
+ export function deriveIssueSessionWaitingReason(params) {
14
+ return derivePatchRelayWaitingReason(params);
15
+ }
16
+ export function deriveIssueSessionWakeReason(params) {
17
+ if (params.pendingRunType === "implementation")
18
+ return "delegated";
19
+ if (params.pendingRunType === "review_fix")
20
+ return "review_changes_requested";
21
+ if (params.pendingRunType === "ci_repair")
22
+ return "settled_red_ci";
23
+ if (params.pendingRunType === "queue_repair")
24
+ return "merge_steward_incident";
25
+ if (params.factoryState === "awaiting_input")
26
+ return "waiting_for_human_reply";
27
+ const reactiveIntent = deriveIssueSessionReactiveIntent({
28
+ prNumber: params.prNumber,
29
+ prState: params.prState,
30
+ prReviewState: params.prReviewState,
31
+ prCheckStatus: params.prCheckStatus,
32
+ latestFailureSource: params.latestFailureSource,
33
+ });
34
+ if (reactiveIntent)
35
+ return reactiveIntent.wakeReason;
36
+ return undefined;
37
+ }
38
+ export function deriveIssueSessionReactiveIntent(params) {
39
+ if (params.activeRunId !== undefined)
40
+ return undefined;
41
+ if (params.prNumber === undefined)
42
+ return undefined;
43
+ if (params.prState && params.prState !== "open")
44
+ return undefined;
45
+ if (params.latestFailureSource === "queue_eviction" || (params.mergeConflictDetected && params.downstreamOwned)) {
46
+ return {
47
+ runType: "queue_repair",
48
+ wakeReason: "merge_steward_incident",
49
+ compatibilityFactoryState: "repairing_queue",
50
+ };
51
+ }
52
+ if (params.prCheckStatus === "failed" || params.prCheckStatus === "failure" || params.latestFailureSource === "branch_ci") {
53
+ return {
54
+ runType: "ci_repair",
55
+ wakeReason: "settled_red_ci",
56
+ compatibilityFactoryState: "repairing_ci",
57
+ };
58
+ }
59
+ if (params.prReviewState === "changes_requested") {
60
+ return {
61
+ runType: "review_fix",
62
+ wakeReason: "review_changes_requested",
63
+ compatibilityFactoryState: "changes_requested",
64
+ };
65
+ }
66
+ return undefined;
67
+ }
68
+ export function isIssueSessionReadyForExecution(params) {
69
+ if (params.activeRunId !== undefined)
70
+ return false;
71
+ if (params.blockedByCount > 0)
72
+ return false;
73
+ if (params.sessionState === "done" || params.sessionState === "failed" || params.sessionState === "waiting_input") {
74
+ return false;
75
+ }
76
+ if (params.hasPendingWake) {
77
+ return true;
78
+ }
79
+ if (!params.hasLegacyPendingRun) {
80
+ return false;
81
+ }
82
+ if (deriveIssueSessionReactiveIntent({
83
+ prNumber: params.prNumber,
84
+ prState: params.prState,
85
+ prReviewState: params.prReviewState,
86
+ prCheckStatus: params.prCheckStatus,
87
+ latestFailureSource: params.latestFailureSource,
88
+ }) === undefined) {
89
+ return false;
90
+ }
91
+ if (params.factoryState === "awaiting_queue"
92
+ || params.factoryState === "awaiting_input"
93
+ || params.factoryState === "done"
94
+ || params.factoryState === "failed"
95
+ || params.factoryState === "escalated") {
96
+ return false;
97
+ }
98
+ return true;
99
+ }