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.
- package/README.md +41 -9
- package/dist/build-info.json +3 -3
- package/dist/cli/args.js +19 -1
- package/dist/cli/commands/issues.js +18 -56
- package/dist/cli/commands/watch.js +5 -0
- package/dist/cli/data.js +160 -47
- package/dist/cli/formatters/text.js +51 -90
- package/dist/cli/help.js +15 -8
- package/dist/cli/index.js +3 -58
- package/dist/cli/operator-client.js +0 -82
- package/dist/cli/watch/App.js +21 -12
- package/dist/cli/watch/HelpBar.js +3 -3
- package/dist/cli/watch/IssueDetailView.js +63 -130
- package/dist/cli/watch/IssueRow.js +82 -27
- package/dist/cli/watch/StatusBar.js +8 -4
- package/dist/cli/watch/detail-rows.js +589 -0
- package/dist/cli/watch/render-rich-text.js +226 -0
- package/dist/cli/watch/state-visualization.js +48 -23
- package/dist/cli/watch/timeline-builder.js +2 -1
- package/dist/cli/watch/use-detail-stream.js +10 -104
- package/dist/cli/watch/use-watch-stream.js +11 -102
- package/dist/cli/watch/watch-state.js +129 -56
- package/dist/codex-thread-utils.js +3 -0
- package/dist/db/migrations.js +239 -2
- package/dist/db.js +628 -39
- package/dist/github-app-token.js +7 -0
- package/dist/github-failure-context.js +44 -1
- package/dist/github-rollup.js +47 -0
- package/dist/github-webhook-handler.js +423 -52
- package/dist/github-webhooks.js +7 -0
- package/dist/http.js +12 -264
- package/dist/idle-reconciliation.js +268 -76
- package/dist/issue-query-service.js +221 -129
- package/dist/issue-session-events.js +151 -0
- package/dist/issue-session.js +99 -0
- package/dist/linear-client.js +39 -25
- package/dist/linear-session-reporting.js +12 -0
- package/dist/linear-session-sync.js +253 -24
- package/dist/linear-workflow.js +33 -0
- package/dist/merge-queue-protocol.js +0 -51
- package/dist/preflight.js +1 -4
- package/dist/queue-health-monitor.js +11 -7
- package/dist/run-orchestrator.js +1364 -147
- package/dist/run-reporting.js +5 -3
- package/dist/service.js +279 -102
- package/dist/status-note.js +56 -0
- package/dist/waiting-reason.js +65 -0
- package/dist/webhook-handler.js +270 -79
- package/package.json +3 -2
- package/dist/cli/commands/feed.js +0 -60
- package/dist/cli/watch/FeedView.js +0 -28
- package/dist/cli/watch/use-feed-stream.js +0 -92
package/dist/run-reporting.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getThreadTurns } from "./codex-thread-utils.js";
|
|
1
2
|
export function extractStageSummary(report) {
|
|
2
3
|
return {
|
|
3
4
|
assistantMessageCount: report.assistantMessages.length,
|
|
@@ -8,7 +9,8 @@ export function extractStageSummary(report) {
|
|
|
8
9
|
};
|
|
9
10
|
}
|
|
10
11
|
export function summarizeCurrentThread(thread) {
|
|
11
|
-
const
|
|
12
|
+
const turns = getThreadTurns(thread);
|
|
13
|
+
const latestTurn = turns.at(-1);
|
|
12
14
|
const latestAgentMessage = latestTurn?.items
|
|
13
15
|
.filter((item) => item.type === "agentMessage")
|
|
14
16
|
.at(-1)?.text;
|
|
@@ -25,7 +27,7 @@ export function summarizeCurrentThread(thread) {
|
|
|
25
27
|
let commandCount = 0;
|
|
26
28
|
let fileChangeCount = 0;
|
|
27
29
|
let toolCallCount = 0;
|
|
28
|
-
for (const turn of
|
|
30
|
+
for (const turn of turns) {
|
|
29
31
|
for (const item of turn.items) {
|
|
30
32
|
if (item.type === "commandExecution") {
|
|
31
33
|
commandCount += 1;
|
|
@@ -57,7 +59,7 @@ export function buildStageReport(run, issue, thread, eventCounts) {
|
|
|
57
59
|
const commands = [];
|
|
58
60
|
const fileChanges = [];
|
|
59
61
|
const toolCalls = [];
|
|
60
|
-
for (const turn of thread
|
|
62
|
+
for (const turn of getThreadTurns(thread)) {
|
|
61
63
|
for (const rawItem of turn.items) {
|
|
62
64
|
const item = rawItem;
|
|
63
65
|
if (item.type === "agentMessage" && typeof item.text === "string") {
|
package/dist/service.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { resolveGitHubAppCredentials, createGitHubAppTokenManager, ensureGhWrapper, } from "./github-app-token.js";
|
|
2
2
|
import { parseGitHubFailureContext, summarizeGitHubFailureContext } from "./github-failure-context.js";
|
|
3
|
+
import { isIssueSessionReadyForExecution } from "./issue-session.js";
|
|
3
4
|
import { GitHubWebhookHandler } from "./github-webhook-handler.js";
|
|
4
5
|
import { IssueQueryService } from "./issue-query-service.js";
|
|
5
6
|
import { DatabaseBackedLinearClientProvider } from "./linear-client.js";
|
|
6
7
|
import { LinearOAuthService } from "./linear-oauth-service.js";
|
|
7
8
|
import { RunOrchestrator } from "./run-orchestrator.js";
|
|
8
9
|
import { OperatorEventFeed } from "./operator-feed.js";
|
|
10
|
+
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
9
11
|
import { buildSessionStatusUrl, createSessionStatusToken, deriveSessionStatusSigningSecret, verifySessionStatusToken, } from "./public-agent-session-status.js";
|
|
10
12
|
import { ServiceRuntime } from "./service-runtime.js";
|
|
11
13
|
import { WebhookHandler } from "./webhook-handler.js";
|
|
12
14
|
import { acceptIncomingWebhook } from "./service-webhooks.js";
|
|
15
|
+
import { deriveIssueStatusNote } from "./status-note.js";
|
|
13
16
|
function parseObjectJson(value) {
|
|
14
17
|
if (!value)
|
|
15
18
|
return undefined;
|
|
@@ -21,19 +24,14 @@ function parseObjectJson(value) {
|
|
|
21
24
|
return undefined;
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const latest = assistantMessages.findLast((value) => typeof value === "string" && value.trim().length > 0);
|
|
33
|
-
if (typeof latest === "string")
|
|
34
|
-
return latest;
|
|
35
|
-
}
|
|
36
|
-
return undefined;
|
|
27
|
+
function shouldSuppressStatusNote(params) {
|
|
28
|
+
if (!params.activeRunType && params.sessionState !== "running")
|
|
29
|
+
return false;
|
|
30
|
+
const note = params.statusNote?.trim().toLowerCase();
|
|
31
|
+
if (!note)
|
|
32
|
+
return true;
|
|
33
|
+
return note === "codex turn was interrupted"
|
|
34
|
+
|| note === "patchrelay received your mention. delegate the issue to patchrelay to start work.";
|
|
37
35
|
}
|
|
38
36
|
export function parseCiSnapshotSummary(snapshotJson) {
|
|
39
37
|
if (!snapshotJson)
|
|
@@ -116,7 +114,7 @@ export class PatchRelayService {
|
|
|
116
114
|
});
|
|
117
115
|
enqueueIssue = (projectId, issueId) => runtime.enqueueIssue(projectId, issueId);
|
|
118
116
|
this.oauthService = new LinearOAuthService(config, { linearInstallations: db.linearInstallations }, logger);
|
|
119
|
-
this.queryService = new IssueQueryService(
|
|
117
|
+
this.queryService = new IssueQueryService(db, codex, this.orchestrator);
|
|
120
118
|
this.runtime = runtime;
|
|
121
119
|
// Optional GitHub App token management for bot identity
|
|
122
120
|
const ghAppCredentials = resolveGitHubAppCredentials();
|
|
@@ -132,6 +130,7 @@ export class PatchRelayService {
|
|
|
132
130
|
});
|
|
133
131
|
}
|
|
134
132
|
async start() {
|
|
133
|
+
this.db.releaseExpiredIssueSessionLeases();
|
|
135
134
|
const repairedInstallations = this.db.linearInstallations.repairProjectInstallations(this.config.projects.map((project) => project.id));
|
|
136
135
|
for (const repair of repairedInstallations) {
|
|
137
136
|
this.logger.info({ projectId: repair.projectId, installationId: repair.installationId, reason: repair.reason }, "Repaired Linear project installation link");
|
|
@@ -166,14 +165,110 @@ export class PatchRelayService {
|
|
|
166
165
|
const identity = this.githubAppTokenManager.botIdentity();
|
|
167
166
|
if (identity) {
|
|
168
167
|
this.orchestrator.botIdentity = identity;
|
|
168
|
+
this.githubWebhookHandler.setPatchRelayAuthorLogins([identity.name]);
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
await this.runtime.start();
|
|
172
|
+
await this.recoverDelegatedIssueStateFromLinear();
|
|
173
|
+
void this.syncKnownAgentSessions().catch((error) => {
|
|
174
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
175
|
+
this.logger.warn({ error: msg }, "Background agent session sync failed");
|
|
176
|
+
});
|
|
172
177
|
}
|
|
173
178
|
async stop() {
|
|
174
179
|
this.githubAppTokenManager?.stop();
|
|
175
180
|
await this.runtime.stop();
|
|
176
181
|
}
|
|
182
|
+
async syncKnownAgentSessions() {
|
|
183
|
+
for (const issue of this.db.listIssues()) {
|
|
184
|
+
if (issue.factoryState === "done") {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const syncedIssue = issue.agentSessionId
|
|
188
|
+
? issue
|
|
189
|
+
: (() => {
|
|
190
|
+
const recoveredAgentSessionId = this.db.findLatestAgentSessionIdForIssue(issue.linearIssueId);
|
|
191
|
+
return recoveredAgentSessionId
|
|
192
|
+
? this.db.upsertIssue({
|
|
193
|
+
projectId: issue.projectId,
|
|
194
|
+
linearIssueId: issue.linearIssueId,
|
|
195
|
+
agentSessionId: recoveredAgentSessionId,
|
|
196
|
+
})
|
|
197
|
+
: issue;
|
|
198
|
+
})();
|
|
199
|
+
if (!syncedIssue.agentSessionId) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const activeRun = syncedIssue.activeRunId ? this.db.getRun(syncedIssue.activeRunId) : undefined;
|
|
203
|
+
await this.orchestrator.linearSync.syncSession(syncedIssue, activeRun ? { activeRunType: activeRun.runType } : undefined);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async recoverDelegatedIssueStateFromLinear() {
|
|
207
|
+
for (const issue of this.db.listIssuesWithAgentSessions()) {
|
|
208
|
+
if (issue.factoryState === "done" || issue.activeRunId !== undefined) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const linear = await this.linearProvider.forProject(issue.projectId).catch(() => undefined);
|
|
212
|
+
if (!linear) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const installation = this.db.linearInstallations.getLinearInstallationForProject(issue.projectId);
|
|
216
|
+
if (!installation?.actorId) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const liveIssue = await linear.getIssue(issue.linearIssueId).catch(() => undefined);
|
|
220
|
+
if (!liveIssue) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
this.db.replaceIssueDependencies({
|
|
224
|
+
projectId: issue.projectId,
|
|
225
|
+
linearIssueId: issue.linearIssueId,
|
|
226
|
+
blockers: liveIssue.blockedBy.map((blocker) => ({
|
|
227
|
+
blockerLinearIssueId: blocker.id,
|
|
228
|
+
...(blocker.identifier ? { blockerIssueKey: blocker.identifier } : {}),
|
|
229
|
+
...(blocker.title ? { blockerTitle: blocker.title } : {}),
|
|
230
|
+
...(blocker.stateName ? { blockerCurrentLinearState: blocker.stateName } : {}),
|
|
231
|
+
...(blocker.stateType ? { blockerCurrentLinearStateType: blocker.stateType } : {}),
|
|
232
|
+
})),
|
|
233
|
+
});
|
|
234
|
+
const delegated = liveIssue.delegateId === installation.actorId;
|
|
235
|
+
const unresolvedBlockers = this.db.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
236
|
+
const shouldRecoverAwaitingInput = delegated
|
|
237
|
+
&& issue.factoryState === "awaiting_input"
|
|
238
|
+
&& this.db.peekIssueSessionWake(issue.projectId, issue.linearIssueId) === undefined;
|
|
239
|
+
const updated = this.db.upsertIssue({
|
|
240
|
+
projectId: issue.projectId,
|
|
241
|
+
linearIssueId: issue.linearIssueId,
|
|
242
|
+
...(liveIssue.identifier ? { issueKey: liveIssue.identifier } : {}),
|
|
243
|
+
...(liveIssue.title ? { title: liveIssue.title } : {}),
|
|
244
|
+
...(liveIssue.description ? { description: liveIssue.description } : {}),
|
|
245
|
+
...(liveIssue.url ? { url: liveIssue.url } : {}),
|
|
246
|
+
...(liveIssue.priority != null ? { priority: liveIssue.priority } : {}),
|
|
247
|
+
...(liveIssue.estimate != null ? { estimate: liveIssue.estimate } : {}),
|
|
248
|
+
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
249
|
+
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
250
|
+
...(shouldRecoverAwaitingInput ? { factoryState: "delegated" } : {}),
|
|
251
|
+
});
|
|
252
|
+
if (!shouldRecoverAwaitingInput) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (unresolvedBlockers === 0) {
|
|
256
|
+
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
257
|
+
projectId: issue.projectId,
|
|
258
|
+
linearIssueId: issue.linearIssueId,
|
|
259
|
+
eventType: "delegated",
|
|
260
|
+
dedupeKey: `delegated:${issue.linearIssueId}`,
|
|
261
|
+
});
|
|
262
|
+
if (this.db.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
263
|
+
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
264
|
+
}
|
|
265
|
+
this.logger.info({ issueKey: updated.issueKey }, "Recovered delegated issue from stale awaiting_input state and re-queued implementation");
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
this.logger.info({ issueKey: updated.issueKey, unresolvedBlockers }, "Recovered delegated blocked issue from stale awaiting_input state");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
177
272
|
async createLinearOAuthStart(params) {
|
|
178
273
|
return await this.oauthService.createStart(params);
|
|
179
274
|
}
|
|
@@ -264,8 +359,8 @@ export class PatchRelayService {
|
|
|
264
359
|
listTrackedIssues() {
|
|
265
360
|
const rows = this.db.connection
|
|
266
361
|
.prepare(`SELECT
|
|
267
|
-
|
|
268
|
-
i.current_linear_state, i.factory_state,
|
|
362
|
+
s.project_id, s.linear_issue_id, s.issue_key, i.title,
|
|
363
|
+
i.current_linear_state, i.factory_state, s.session_state, s.waiting_reason, s.summary_text, s.updated_at,
|
|
269
364
|
i.pending_run_type,
|
|
270
365
|
i.pr_number, i.pr_review_state, i.pr_check_status,
|
|
271
366
|
i.last_github_ci_snapshot_json,
|
|
@@ -278,14 +373,21 @@ export class PatchRelayService {
|
|
|
278
373
|
latest_run.status AS latest_run_status,
|
|
279
374
|
latest_run.summary_json AS latest_run_summary_json,
|
|
280
375
|
latest_run.report_json AS latest_run_report_json,
|
|
376
|
+
(
|
|
377
|
+
SELECT COUNT(*)
|
|
378
|
+
FROM issue_session_events e
|
|
379
|
+
WHERE e.project_id = s.project_id
|
|
380
|
+
AND e.linear_issue_id = s.linear_issue_id
|
|
381
|
+
AND e.processed_at IS NULL
|
|
382
|
+
) AS pending_session_event_count,
|
|
281
383
|
(
|
|
282
384
|
SELECT COUNT(*)
|
|
283
385
|
FROM issue_dependencies d
|
|
284
386
|
LEFT JOIN issues blockers
|
|
285
387
|
ON blockers.project_id = d.project_id
|
|
286
388
|
AND blockers.linear_issue_id = d.blocker_linear_issue_id
|
|
287
|
-
WHERE d.project_id =
|
|
288
|
-
AND d.linear_issue_id =
|
|
389
|
+
WHERE d.project_id = s.project_id
|
|
390
|
+
AND d.linear_issue_id = s.linear_issue_id
|
|
289
391
|
AND (
|
|
290
392
|
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
|
|
291
393
|
AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
|
|
@@ -297,45 +399,99 @@ export class PatchRelayService {
|
|
|
297
399
|
LEFT JOIN issues blockers
|
|
298
400
|
ON blockers.project_id = d.project_id
|
|
299
401
|
AND blockers.linear_issue_id = d.blocker_linear_issue_id
|
|
300
|
-
WHERE d.project_id =
|
|
301
|
-
AND d.linear_issue_id =
|
|
402
|
+
WHERE d.project_id = s.project_id
|
|
403
|
+
AND d.linear_issue_id = s.linear_issue_id
|
|
302
404
|
AND (
|
|
303
405
|
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
|
|
304
406
|
AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
|
|
305
407
|
)
|
|
306
408
|
) AS blocked_by_keys_json
|
|
307
|
-
FROM
|
|
308
|
-
LEFT JOIN
|
|
409
|
+
FROM issue_sessions s
|
|
410
|
+
LEFT JOIN issues i
|
|
411
|
+
ON i.project_id = s.project_id
|
|
412
|
+
AND i.linear_issue_id = s.linear_issue_id
|
|
413
|
+
LEFT JOIN runs active_run ON active_run.id = COALESCE(s.active_run_id, i.active_run_id)
|
|
309
414
|
LEFT JOIN runs latest_run ON latest_run.id = (
|
|
310
415
|
SELECT r.id FROM runs r
|
|
311
|
-
WHERE r.project_id =
|
|
416
|
+
WHERE r.project_id = s.project_id AND r.linear_issue_id = s.linear_issue_id
|
|
312
417
|
ORDER BY r.id DESC LIMIT 1
|
|
313
418
|
)
|
|
314
|
-
ORDER BY
|
|
419
|
+
ORDER BY s.updated_at DESC, s.issue_key ASC`)
|
|
315
420
|
.all();
|
|
316
421
|
return rows.map((row) => {
|
|
317
422
|
const failureContext = parseGitHubFailureContext(typeof row.last_github_failure_context_json === "string" ? row.last_github_failure_context_json : undefined);
|
|
318
423
|
const prChecksSummary = parseCiSnapshotSummary(typeof row.last_github_ci_snapshot_json === "string" ? row.last_github_ci_snapshot_json : undefined);
|
|
319
|
-
const statusNote = extractStatusNote(typeof row.latest_run_summary_json === "string" ? row.latest_run_summary_json : undefined, typeof row.latest_run_report_json === "string" ? row.latest_run_report_json : undefined);
|
|
320
424
|
const blockedByKeys = parseStringArray(typeof row.blocked_by_keys_json === "string" ? row.blocked_by_keys_json : undefined);
|
|
321
425
|
const blockedByCount = Number(row.blocked_by_count ?? 0);
|
|
322
|
-
const
|
|
426
|
+
const hasPendingSessionEvents = Number(row.pending_session_event_count ?? 0) > 0;
|
|
427
|
+
const hasPendingWake = hasPendingSessionEvents
|
|
428
|
+
|| this.db.peekIssueSessionWake(String(row.project_id), String(row.linear_issue_id)) !== undefined;
|
|
429
|
+
const readyForExecution = isIssueSessionReadyForExecution({
|
|
430
|
+
...(typeof row.session_state === "string" ? { sessionState: String(row.session_state) } : {}),
|
|
431
|
+
factoryState: String(row.factory_state ?? "delegated"),
|
|
432
|
+
...(row.active_run_type !== null ? { activeRunId: 1 } : {}),
|
|
433
|
+
blockedByCount,
|
|
434
|
+
hasPendingWake,
|
|
435
|
+
hasLegacyPendingRun: row.pending_run_type !== null && row.pending_run_type !== undefined,
|
|
436
|
+
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
437
|
+
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
438
|
+
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
439
|
+
...(row.pr_check_status !== null ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
440
|
+
...(row.last_github_failure_source !== null ? { latestFailureSource: String(row.last_github_failure_source) } : {}),
|
|
441
|
+
});
|
|
323
442
|
const failureSummary = summarizeGitHubFailureContext(failureContext);
|
|
324
|
-
const
|
|
325
|
-
?
|
|
326
|
-
:
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
:
|
|
443
|
+
const sessionWaitingReason = typeof row.waiting_reason === "string" && row.waiting_reason.trim().length > 0
|
|
444
|
+
? row.waiting_reason
|
|
445
|
+
: undefined;
|
|
446
|
+
const sessionSummary = typeof row.summary_text === "string" && row.summary_text.trim().length > 0
|
|
447
|
+
? row.summary_text
|
|
448
|
+
: undefined;
|
|
449
|
+
const waitingReason = sessionWaitingReason ?? derivePatchRelayWaitingReason({
|
|
450
|
+
...(row.active_run_type !== null ? { activeRunType: String(row.active_run_type) } : {}),
|
|
451
|
+
blockedByKeys,
|
|
452
|
+
factoryState: String(row.factory_state ?? "delegated"),
|
|
453
|
+
...(row.pending_run_type !== null ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
454
|
+
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
455
|
+
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
456
|
+
...(row.pr_check_status !== null ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
457
|
+
...(row.last_github_failure_check_name !== null ? { latestFailureCheckName: String(row.last_github_failure_check_name) } : {}),
|
|
458
|
+
});
|
|
459
|
+
const latestRun = row.latest_run_type !== null && row.latest_run_status !== null
|
|
460
|
+
? {
|
|
461
|
+
id: 0,
|
|
462
|
+
issueId: 0,
|
|
463
|
+
projectId: String(row.project_id),
|
|
464
|
+
linearIssueId: String(row.linear_issue_id),
|
|
465
|
+
runType: String(row.latest_run_type),
|
|
466
|
+
status: String(row.latest_run_status),
|
|
467
|
+
...(typeof row.latest_run_summary_json === "string" ? { summaryJson: row.latest_run_summary_json } : {}),
|
|
468
|
+
...(typeof row.latest_run_report_json === "string" ? { reportJson: row.latest_run_report_json } : {}),
|
|
469
|
+
startedAt: String(row.updated_at),
|
|
470
|
+
}
|
|
471
|
+
: undefined;
|
|
472
|
+
const latestEvent = this.db.listIssueSessionEvents(String(row.project_id), String(row.linear_issue_id), { limit: 1 }).at(-1);
|
|
473
|
+
const statusNoteCandidate = deriveIssueStatusNote({
|
|
474
|
+
issue: { factoryState: String(row.factory_state ?? "delegated") },
|
|
475
|
+
sessionSummary,
|
|
476
|
+
latestRun: latestRun,
|
|
477
|
+
latestEvent,
|
|
478
|
+
failureSummary,
|
|
479
|
+
blockedByKeys,
|
|
480
|
+
waitingReason,
|
|
481
|
+
}) ?? waitingReason;
|
|
482
|
+
const statusNoteForReturn = shouldSuppressStatusNote({
|
|
483
|
+
activeRunType: row.active_run_type,
|
|
484
|
+
sessionState: row.session_state,
|
|
485
|
+
statusNote: statusNoteCandidate,
|
|
486
|
+
})
|
|
487
|
+
? undefined
|
|
488
|
+
: statusNoteCandidate;
|
|
334
489
|
return {
|
|
335
490
|
...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
|
|
336
491
|
...(row.title !== null ? { title: String(row.title) } : {}),
|
|
337
|
-
...(
|
|
492
|
+
...(statusNoteForReturn ? { statusNote: statusNoteForReturn } : {}),
|
|
338
493
|
projectId: String(row.project_id),
|
|
494
|
+
...(row.session_state !== null ? { sessionState: String(row.session_state) } : {}),
|
|
339
495
|
factoryState: String(row.factory_state ?? "delegated"),
|
|
340
496
|
blockedByCount,
|
|
341
497
|
blockedByKeys,
|
|
@@ -354,45 +510,11 @@ export class PatchRelayService {
|
|
|
354
510
|
...(row.last_github_failure_check_name !== null ? { latestFailureCheckName: String(row.last_github_failure_check_name) } : {}),
|
|
355
511
|
...(failureContext?.stepName ? { latestFailureStepName: failureContext.stepName } : {}),
|
|
356
512
|
...(failureContext?.summary ? { latestFailureSummary: failureContext.summary } : {}),
|
|
513
|
+
...(waitingReason ? { waitingReason } : {}),
|
|
357
514
|
updatedAt: String(row.updated_at),
|
|
358
515
|
};
|
|
359
516
|
});
|
|
360
517
|
}
|
|
361
|
-
subscribeCodexNotifications(listener) {
|
|
362
|
-
let trackedThreadId;
|
|
363
|
-
const handler = (notification) => {
|
|
364
|
-
let threadId = typeof notification.params.threadId === "string"
|
|
365
|
-
? notification.params.threadId
|
|
366
|
-
: typeof notification.params.thread === "object" && notification.params.thread !== null && "id" in notification.params.thread
|
|
367
|
-
? String(notification.params.thread.id)
|
|
368
|
-
: undefined;
|
|
369
|
-
// Item-level notifications lack threadId — use the tracked one from turn/started
|
|
370
|
-
if (!threadId)
|
|
371
|
-
threadId = trackedThreadId;
|
|
372
|
-
if (notification.method === "turn/started" && threadId)
|
|
373
|
-
trackedThreadId = threadId;
|
|
374
|
-
if (notification.method === "turn/completed")
|
|
375
|
-
trackedThreadId = undefined;
|
|
376
|
-
let issueKey;
|
|
377
|
-
let runId;
|
|
378
|
-
if (threadId) {
|
|
379
|
-
const run = this.db.getRunByThreadId(threadId);
|
|
380
|
-
if (run) {
|
|
381
|
-
runId = run.id;
|
|
382
|
-
const issue = this.db.getIssue(run.projectId, run.linearIssueId);
|
|
383
|
-
issueKey = issue?.issueKey ?? undefined;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
listener({
|
|
387
|
-
method: notification.method,
|
|
388
|
-
params: notification.params,
|
|
389
|
-
...(issueKey ? { issueKey } : {}),
|
|
390
|
-
...(runId !== undefined ? { runId } : {}),
|
|
391
|
-
});
|
|
392
|
-
};
|
|
393
|
-
this.codex.on("notification", handler);
|
|
394
|
-
return () => { this.codex.off("notification", handler); };
|
|
395
|
-
}
|
|
396
518
|
async promptIssue(issueKey, text, source = "watch") {
|
|
397
519
|
const issue = this.db.getIssueByKey(issueKey);
|
|
398
520
|
if (!issue)
|
|
@@ -410,14 +532,13 @@ export class PatchRelayService {
|
|
|
410
532
|
});
|
|
411
533
|
// If no active run, queue as pending context for the next run
|
|
412
534
|
if (!issue.activeRunId) {
|
|
413
|
-
|
|
414
|
-
? JSON.parse(issue.pendingRunContextJson)
|
|
415
|
-
: {};
|
|
416
|
-
this.db.upsertIssue({
|
|
535
|
+
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
417
536
|
projectId: issue.projectId,
|
|
418
537
|
linearIssueId: issue.linearIssueId,
|
|
419
|
-
|
|
538
|
+
eventType: "operator_prompt",
|
|
539
|
+
eventJson: JSON.stringify({ text, source }),
|
|
420
540
|
});
|
|
541
|
+
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
421
542
|
return { delivered: false, queued: true };
|
|
422
543
|
}
|
|
423
544
|
const run = this.db.getRun(issue.activeRunId);
|
|
@@ -436,14 +557,13 @@ export class PatchRelayService {
|
|
|
436
557
|
// Turn may have completed between check and steer — queue for next run
|
|
437
558
|
const msg = error instanceof Error ? error.message : String(error);
|
|
438
559
|
this.logger.warn({ issueKey, error: msg }, "steerTurn failed, queuing prompt for next run");
|
|
439
|
-
|
|
440
|
-
? JSON.parse(issue.pendingRunContextJson)
|
|
441
|
-
: {};
|
|
442
|
-
this.db.upsertIssue({
|
|
560
|
+
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
443
561
|
projectId: issue.projectId,
|
|
444
562
|
linearIssueId: issue.linearIssueId,
|
|
445
|
-
|
|
563
|
+
eventType: "operator_prompt",
|
|
564
|
+
eventJson: JSON.stringify({ text, source }),
|
|
446
565
|
});
|
|
566
|
+
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
447
567
|
return { delivered: false, queued: true };
|
|
448
568
|
}
|
|
449
569
|
}
|
|
@@ -466,7 +586,14 @@ export class PatchRelayService {
|
|
|
466
586
|
// Turn may already be done
|
|
467
587
|
}
|
|
468
588
|
}
|
|
469
|
-
this.db.
|
|
589
|
+
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
590
|
+
projectId: issue.projectId,
|
|
591
|
+
linearIssueId: issue.linearIssueId,
|
|
592
|
+
eventType: "stop_requested",
|
|
593
|
+
dedupeKey: `operator_stop:${issue.linearIssueId}`,
|
|
594
|
+
});
|
|
595
|
+
this.db.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
596
|
+
this.db.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
470
597
|
projectId: issue.projectId,
|
|
471
598
|
linearIssueId: issue.linearIssueId,
|
|
472
599
|
factoryState: "awaiting_input",
|
|
@@ -488,13 +615,21 @@ export class PatchRelayService {
|
|
|
488
615
|
if (issue.activeRunId)
|
|
489
616
|
return { error: "Issue already has an active run" };
|
|
490
617
|
if (issue.prState === "merged") {
|
|
491
|
-
this.db.
|
|
618
|
+
this.db.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
619
|
+
projectId: issue.projectId,
|
|
620
|
+
linearIssueId: issue.linearIssueId,
|
|
621
|
+
factoryState: "done",
|
|
622
|
+
});
|
|
492
623
|
return { issueKey, runType: "none" };
|
|
493
624
|
}
|
|
494
625
|
// Infer run type from current state instead of always resetting to implementation
|
|
495
626
|
let runType = "implementation";
|
|
496
627
|
let factoryState = "delegated";
|
|
497
|
-
if (issue.prNumber &&
|
|
628
|
+
if (issue.prNumber && issue.lastGitHubFailureSource === "queue_eviction") {
|
|
629
|
+
runType = "queue_repair";
|
|
630
|
+
factoryState = "repairing_queue";
|
|
631
|
+
}
|
|
632
|
+
else if (issue.prNumber && (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure" || issue.lastGitHubFailureSource === "branch_ci")) {
|
|
498
633
|
runType = "ci_repair";
|
|
499
634
|
factoryState = "repairing_ci";
|
|
500
635
|
}
|
|
@@ -507,10 +642,10 @@ export class PatchRelayService {
|
|
|
507
642
|
runType = "implementation";
|
|
508
643
|
factoryState = "implementing";
|
|
509
644
|
}
|
|
510
|
-
this.
|
|
645
|
+
this.appendOperatorRetryEvent(issue, runType);
|
|
646
|
+
this.db.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
511
647
|
projectId: issue.projectId,
|
|
512
648
|
linearIssueId: issue.linearIssueId,
|
|
513
|
-
pendingRunType: runType,
|
|
514
649
|
factoryState: factoryState,
|
|
515
650
|
});
|
|
516
651
|
this.feed.publish({
|
|
@@ -522,14 +657,65 @@ export class PatchRelayService {
|
|
|
522
657
|
status: "retry",
|
|
523
658
|
summary: `Retry queued: ${runType}`,
|
|
524
659
|
});
|
|
525
|
-
this.
|
|
660
|
+
if (this.db.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
661
|
+
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
662
|
+
}
|
|
526
663
|
return { issueKey, runType };
|
|
527
664
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
665
|
+
appendOperatorRetryEvent(issue, runType) {
|
|
666
|
+
if (runType === "queue_repair") {
|
|
667
|
+
const queueIncident = parseObjectJson(issue.lastQueueIncidentJson);
|
|
668
|
+
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson);
|
|
669
|
+
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
670
|
+
projectId: issue.projectId,
|
|
671
|
+
linearIssueId: issue.linearIssueId,
|
|
672
|
+
eventType: "merge_steward_incident",
|
|
673
|
+
eventJson: JSON.stringify({
|
|
674
|
+
...(queueIncident ?? {}),
|
|
675
|
+
...(failureContext ?? {}),
|
|
676
|
+
source: "operator_retry",
|
|
677
|
+
}),
|
|
678
|
+
dedupeKey: `operator_retry:queue_repair:${issue.linearIssueId}:${issue.prHeadSha ?? issue.lastGitHubFailureHeadSha ?? "unknown-sha"}`,
|
|
679
|
+
});
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (runType === "ci_repair") {
|
|
683
|
+
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson);
|
|
684
|
+
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
685
|
+
projectId: issue.projectId,
|
|
686
|
+
linearIssueId: issue.linearIssueId,
|
|
687
|
+
eventType: "settled_red_ci",
|
|
688
|
+
eventJson: JSON.stringify({
|
|
689
|
+
...(failureContext ?? {}),
|
|
690
|
+
source: "operator_retry",
|
|
691
|
+
}),
|
|
692
|
+
dedupeKey: `operator_retry:ci_repair:${issue.linearIssueId}:${issue.lastGitHubFailureSignature ?? issue.prHeadSha ?? "unknown-sha"}`,
|
|
693
|
+
});
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (runType === "review_fix") {
|
|
697
|
+
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
698
|
+
projectId: issue.projectId,
|
|
699
|
+
linearIssueId: issue.linearIssueId,
|
|
700
|
+
eventType: "review_changes_requested",
|
|
701
|
+
eventJson: JSON.stringify({
|
|
702
|
+
reviewBody: "Operator requested retry of review-fix work.",
|
|
703
|
+
source: "operator_retry",
|
|
704
|
+
}),
|
|
705
|
+
dedupeKey: `operator_retry:review_fix:${issue.linearIssueId}:${issue.prHeadSha ?? "unknown-sha"}`,
|
|
706
|
+
});
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
710
|
+
projectId: issue.projectId,
|
|
711
|
+
linearIssueId: issue.linearIssueId,
|
|
712
|
+
eventType: "delegated",
|
|
713
|
+
eventJson: JSON.stringify({
|
|
714
|
+
promptContext: "Operator requested retry of PatchRelay work.",
|
|
715
|
+
source: "operator_retry",
|
|
716
|
+
}),
|
|
717
|
+
dedupeKey: `operator_retry:implementation:${issue.linearIssueId}`,
|
|
718
|
+
});
|
|
533
719
|
}
|
|
534
720
|
async acceptWebhook(params) {
|
|
535
721
|
const result = await acceptIncomingWebhook({
|
|
@@ -577,15 +763,6 @@ export class PatchRelayService {
|
|
|
577
763
|
async getIssueOverview(issueKey) {
|
|
578
764
|
return await this.queryService.getIssueOverview(issueKey);
|
|
579
765
|
}
|
|
580
|
-
async getIssueReport(issueKey) {
|
|
581
|
-
return await this.queryService.getIssueReport(issueKey);
|
|
582
|
-
}
|
|
583
|
-
async getIssueTimeline(issueKey) {
|
|
584
|
-
return await this.queryService.getIssueTimeline(issueKey);
|
|
585
|
-
}
|
|
586
|
-
async getRunEvents(issueKey, runId) {
|
|
587
|
-
return await this.queryService.getRunEvents(issueKey, runId);
|
|
588
|
-
}
|
|
589
766
|
async getActiveRunStatus(issueKey) {
|
|
590
767
|
return await this.orchestrator.getActiveRunStatus(issueKey);
|
|
591
768
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { extractLatestAssistantSummary } from "./issue-session-events.js";
|
|
2
|
+
function clean(value) {
|
|
3
|
+
const trimmed = value?.trim();
|
|
4
|
+
return trimmed ? trimmed : undefined;
|
|
5
|
+
}
|
|
6
|
+
function eventStatusNote(event) {
|
|
7
|
+
if (!event)
|
|
8
|
+
return undefined;
|
|
9
|
+
switch (event.eventType) {
|
|
10
|
+
case "stop_requested":
|
|
11
|
+
return "Operator stopped the run. Use retry or delegate again to resume.";
|
|
12
|
+
case "undelegated":
|
|
13
|
+
return "Issue was un-delegated from PatchRelay. Delegate it again to resume.";
|
|
14
|
+
case "issue_removed":
|
|
15
|
+
return "Issue was removed from Linear.";
|
|
16
|
+
case "pr_closed":
|
|
17
|
+
return "Pull request was closed without merging.";
|
|
18
|
+
case "pr_merged":
|
|
19
|
+
return "Pull request merged successfully.";
|
|
20
|
+
default:
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function deriveIssueStatusNote(params) {
|
|
25
|
+
const blockedByKeys = (params.blockedByKeys ?? []).filter((value) => value.trim().length > 0);
|
|
26
|
+
if (blockedByKeys.length > 0) {
|
|
27
|
+
return `Blocked by ${blockedByKeys.join(", ")}`;
|
|
28
|
+
}
|
|
29
|
+
const sessionSummary = clean(params.sessionSummary);
|
|
30
|
+
const latestRunNote = clean(extractLatestAssistantSummary(params.latestRun));
|
|
31
|
+
const latestEventNote = clean(eventStatusNote(params.latestEvent));
|
|
32
|
+
const failureSummary = clean(params.failureSummary);
|
|
33
|
+
const waitingReason = clean(params.waitingReason);
|
|
34
|
+
let note;
|
|
35
|
+
switch (params.issue.factoryState) {
|
|
36
|
+
case "awaiting_input":
|
|
37
|
+
note = latestRunNote ?? latestEventNote ?? sessionSummary;
|
|
38
|
+
break;
|
|
39
|
+
case "failed":
|
|
40
|
+
case "escalated":
|
|
41
|
+
note = latestEventNote ?? failureSummary ?? latestRunNote ?? sessionSummary;
|
|
42
|
+
break;
|
|
43
|
+
case "repairing_ci":
|
|
44
|
+
case "repairing_queue":
|
|
45
|
+
note = failureSummary ?? sessionSummary ?? latestRunNote;
|
|
46
|
+
break;
|
|
47
|
+
default:
|
|
48
|
+
note = sessionSummary ?? latestRunNote ?? failureSummary;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
if (!note)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (waitingReason && note === waitingReason)
|
|
54
|
+
return undefined;
|
|
55
|
+
return note;
|
|
56
|
+
}
|