patchrelay 0.36.18 → 0.36.19
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/build-info.json +3 -3
- package/dist/linear-agent-session-client.js +109 -0
- package/dist/linear-progress-reporter.js +185 -0
- package/dist/linear-session-sync.js +23 -519
- package/dist/linear-status-comment-sync.js +152 -0
- package/dist/linear-workflow-state-sync.js +103 -0
- package/dist/no-pr-completion-check.js +199 -0
- package/dist/operator-retry-event.js +58 -0
- package/dist/run-finalizer.js +72 -237
- package/dist/service-issue-actions.js +164 -0
- package/dist/service-startup-recovery.js +104 -0
- package/dist/service.js +15 -556
- package/dist/tracked-issue-list-query.js +259 -0
- package/package.json +1 -1
package/dist/service.js
CHANGED
|
@@ -1,87 +1,17 @@
|
|
|
1
1
|
import { resolveGitHubAppCredentials, createGitHubAppTokenManager, ensureGhWrapper, } from "./github-app-token.js";
|
|
2
|
-
import { parseGitHubFailureContext, summarizeGitHubFailureContext } from "./github-failure-context.js";
|
|
3
|
-
import { isIssueSessionReadyForExecution } from "./issue-session.js";
|
|
4
2
|
import { GitHubWebhookHandler } from "./github-webhook-handler.js";
|
|
5
3
|
import { IssueQueryService } from "./issue-query-service.js";
|
|
6
4
|
import { DatabaseBackedLinearClientProvider } from "./linear-client.js";
|
|
7
5
|
import { LinearOAuthService } from "./linear-oauth-service.js";
|
|
8
6
|
import { RunOrchestrator } from "./run-orchestrator.js";
|
|
9
7
|
import { OperatorEventFeed } from "./operator-feed.js";
|
|
10
|
-
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
11
8
|
import { buildSessionStatusUrl, createSessionStatusToken, deriveSessionStatusSigningSecret, verifySessionStatusToken, } from "./public-agent-session-status.js";
|
|
12
9
|
import { ServiceRuntime } from "./service-runtime.js";
|
|
10
|
+
import { ServiceIssueActions } from "./service-issue-actions.js";
|
|
11
|
+
import { ServiceStartupRecovery } from "./service-startup-recovery.js";
|
|
13
12
|
import { WebhookHandler } from "./webhook-handler.js";
|
|
14
13
|
import { acceptIncomingWebhook } from "./service-webhooks.js";
|
|
15
|
-
import {
|
|
16
|
-
function parseObjectJson(value) {
|
|
17
|
-
if (!value)
|
|
18
|
-
return undefined;
|
|
19
|
-
try {
|
|
20
|
-
const parsed = JSON.parse(value);
|
|
21
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
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.startsWith("zombie: never started")
|
|
35
|
-
|| note === "stale thread after restart"
|
|
36
|
-
|| note === "patchrelay received your mention. delegate the issue to patchrelay to start work.";
|
|
37
|
-
}
|
|
38
|
-
export function parseCiSnapshotSummary(snapshotJson) {
|
|
39
|
-
if (!snapshotJson)
|
|
40
|
-
return undefined;
|
|
41
|
-
try {
|
|
42
|
-
const snapshot = JSON.parse(snapshotJson);
|
|
43
|
-
const rawChecks = Array.isArray(snapshot.checks) ? snapshot.checks : [];
|
|
44
|
-
const checks = collapseEffectiveChecks(rawChecks);
|
|
45
|
-
if (checks.length === 0)
|
|
46
|
-
return undefined;
|
|
47
|
-
let passed = 0;
|
|
48
|
-
let failed = 0;
|
|
49
|
-
let pending = 0;
|
|
50
|
-
const failedNames = [];
|
|
51
|
-
for (const check of checks) {
|
|
52
|
-
if (check.status === "success")
|
|
53
|
-
passed++;
|
|
54
|
-
else if (check.status === "failure") {
|
|
55
|
-
failed++;
|
|
56
|
-
failedNames.push(check.name);
|
|
57
|
-
}
|
|
58
|
-
else
|
|
59
|
-
pending++;
|
|
60
|
-
}
|
|
61
|
-
return {
|
|
62
|
-
total: checks.length,
|
|
63
|
-
completed: passed + failed,
|
|
64
|
-
passed,
|
|
65
|
-
failed,
|
|
66
|
-
pending,
|
|
67
|
-
overall: snapshot.gateCheckStatus,
|
|
68
|
-
...(failedNames.length > 0 ? { failedNames } : {}),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function collapseEffectiveChecks(checks) {
|
|
76
|
-
const effective = new Map();
|
|
77
|
-
for (const check of checks) {
|
|
78
|
-
const name = typeof check?.name === "string" ? check.name.trim() : "";
|
|
79
|
-
if (!name || effective.has(name))
|
|
80
|
-
continue;
|
|
81
|
-
effective.set(name, check);
|
|
82
|
-
}
|
|
83
|
-
return [...effective.values()];
|
|
84
|
-
}
|
|
14
|
+
import { parseStringArray, TrackedIssueListQuery } from "./tracked-issue-list-query.js";
|
|
85
15
|
export class PatchRelayService {
|
|
86
16
|
config;
|
|
87
17
|
db;
|
|
@@ -96,6 +26,9 @@ export class PatchRelayService {
|
|
|
96
26
|
queryService;
|
|
97
27
|
runtime;
|
|
98
28
|
feed;
|
|
29
|
+
issueActions;
|
|
30
|
+
startupRecovery;
|
|
31
|
+
trackedIssueListQuery;
|
|
99
32
|
constructor(config, db, codex, linearProvider, logger) {
|
|
100
33
|
this.config = config;
|
|
101
34
|
this.db = db;
|
|
@@ -118,6 +51,9 @@ export class PatchRelayService {
|
|
|
118
51
|
this.oauthService = new LinearOAuthService(config, { linearInstallations: db.linearInstallations }, logger);
|
|
119
52
|
this.queryService = new IssueQueryService(db, codex, this.orchestrator);
|
|
120
53
|
this.runtime = runtime;
|
|
54
|
+
this.issueActions = new ServiceIssueActions(db, codex, runtime, this.feed, logger);
|
|
55
|
+
this.startupRecovery = new ServiceStartupRecovery(db, this.linearProvider, this.orchestrator.linearSync, (projectId, issueId) => runtime.enqueueIssue(projectId, issueId), logger);
|
|
56
|
+
this.trackedIssueListQuery = new TrackedIssueListQuery(db);
|
|
121
57
|
// Optional GitHub App token management for bot identity
|
|
122
58
|
const ghAppCredentials = resolveGitHubAppCredentials();
|
|
123
59
|
if (ghAppCredentials) {
|
|
@@ -171,8 +107,8 @@ export class PatchRelayService {
|
|
|
171
107
|
}
|
|
172
108
|
}
|
|
173
109
|
await this.runtime.start();
|
|
174
|
-
await this.recoverDelegatedIssueStateFromLinear();
|
|
175
|
-
void this.syncKnownAgentSessions().catch((error) => {
|
|
110
|
+
await this.startupRecovery.recoverDelegatedIssueStateFromLinear();
|
|
111
|
+
void this.startupRecovery.syncKnownAgentSessions().catch((error) => {
|
|
176
112
|
const msg = error instanceof Error ? error.message : String(error);
|
|
177
113
|
this.logger.warn({ error: msg }, "Background agent session sync failed");
|
|
178
114
|
});
|
|
@@ -181,96 +117,6 @@ export class PatchRelayService {
|
|
|
181
117
|
this.githubAppTokenManager?.stop();
|
|
182
118
|
await this.runtime.stop();
|
|
183
119
|
}
|
|
184
|
-
async syncKnownAgentSessions() {
|
|
185
|
-
for (const issue of this.db.issues.listIssues()) {
|
|
186
|
-
if (issue.factoryState === "done") {
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
const syncedIssue = issue.agentSessionId
|
|
190
|
-
? issue
|
|
191
|
-
: (() => {
|
|
192
|
-
const recoveredAgentSessionId = this.db.webhookEvents.findLatestAgentSessionIdForIssue(issue.linearIssueId);
|
|
193
|
-
return recoveredAgentSessionId
|
|
194
|
-
? this.db.issues.upsertIssue({
|
|
195
|
-
projectId: issue.projectId,
|
|
196
|
-
linearIssueId: issue.linearIssueId,
|
|
197
|
-
agentSessionId: recoveredAgentSessionId,
|
|
198
|
-
})
|
|
199
|
-
: issue;
|
|
200
|
-
})();
|
|
201
|
-
if (!syncedIssue.agentSessionId) {
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
const activeRun = syncedIssue.activeRunId ? this.db.runs.getRunById(syncedIssue.activeRunId) : undefined;
|
|
205
|
-
await this.orchestrator.linearSync.syncSession(syncedIssue, activeRun ? { activeRunType: activeRun.runType } : undefined);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
async recoverDelegatedIssueStateFromLinear() {
|
|
209
|
-
for (const issue of this.db.issues.listIssuesWithAgentSessions()) {
|
|
210
|
-
if (issue.factoryState === "done" || issue.activeRunId !== undefined) {
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
const linear = await this.linearProvider.forProject(issue.projectId).catch(() => undefined);
|
|
214
|
-
if (!linear) {
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
const installation = this.db.linearInstallations.getLinearInstallationForProject(issue.projectId);
|
|
218
|
-
if (!installation?.actorId) {
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
const liveIssue = await linear.getIssue(issue.linearIssueId).catch(() => undefined);
|
|
222
|
-
if (!liveIssue) {
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
this.db.issues.replaceIssueDependencies({
|
|
226
|
-
projectId: issue.projectId,
|
|
227
|
-
linearIssueId: issue.linearIssueId,
|
|
228
|
-
blockers: liveIssue.blockedBy.map((blocker) => ({
|
|
229
|
-
blockerLinearIssueId: blocker.id,
|
|
230
|
-
...(blocker.identifier ? { blockerIssueKey: blocker.identifier } : {}),
|
|
231
|
-
...(blocker.title ? { blockerTitle: blocker.title } : {}),
|
|
232
|
-
...(blocker.stateName ? { blockerCurrentLinearState: blocker.stateName } : {}),
|
|
233
|
-
...(blocker.stateType ? { blockerCurrentLinearStateType: blocker.stateType } : {}),
|
|
234
|
-
})),
|
|
235
|
-
});
|
|
236
|
-
const delegated = liveIssue.delegateId === installation.actorId;
|
|
237
|
-
const unresolvedBlockers = this.db.issues.countUnresolvedBlockers(issue.projectId, issue.linearIssueId);
|
|
238
|
-
const shouldRecoverAwaitingInput = delegated
|
|
239
|
-
&& issue.factoryState === "awaiting_input"
|
|
240
|
-
&& this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) === undefined;
|
|
241
|
-
const updated = this.db.issues.upsertIssue({
|
|
242
|
-
projectId: issue.projectId,
|
|
243
|
-
linearIssueId: issue.linearIssueId,
|
|
244
|
-
...(liveIssue.identifier ? { issueKey: liveIssue.identifier } : {}),
|
|
245
|
-
...(liveIssue.title ? { title: liveIssue.title } : {}),
|
|
246
|
-
...(liveIssue.description ? { description: liveIssue.description } : {}),
|
|
247
|
-
...(liveIssue.url ? { url: liveIssue.url } : {}),
|
|
248
|
-
...(liveIssue.priority != null ? { priority: liveIssue.priority } : {}),
|
|
249
|
-
...(liveIssue.estimate != null ? { estimate: liveIssue.estimate } : {}),
|
|
250
|
-
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
251
|
-
...(liveIssue.stateType ? { currentLinearStateType: liveIssue.stateType } : {}),
|
|
252
|
-
...(shouldRecoverAwaitingInput ? { factoryState: "delegated" } : {}),
|
|
253
|
-
});
|
|
254
|
-
if (!shouldRecoverAwaitingInput) {
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
if (unresolvedBlockers === 0) {
|
|
258
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
259
|
-
projectId: issue.projectId,
|
|
260
|
-
linearIssueId: issue.linearIssueId,
|
|
261
|
-
eventType: "delegated",
|
|
262
|
-
dedupeKey: `delegated:${issue.linearIssueId}`,
|
|
263
|
-
});
|
|
264
|
-
if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
265
|
-
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
266
|
-
}
|
|
267
|
-
this.logger.info({ issueKey: updated.issueKey }, "Recovered delegated issue from stale awaiting_input state and re-queued implementation");
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
this.logger.info({ issueKey: updated.issueKey, unresolvedBlockers }, "Recovered delegated blocked issue from stale awaiting_input state");
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
120
|
async createLinearOAuthStart(params) {
|
|
275
121
|
return await this.oauthService.createStart(params);
|
|
276
122
|
}
|
|
@@ -359,392 +205,16 @@ export class PatchRelayService {
|
|
|
359
205
|
return this.runtime.getReadiness();
|
|
360
206
|
}
|
|
361
207
|
listTrackedIssues() {
|
|
362
|
-
|
|
363
|
-
.prepare(`SELECT
|
|
364
|
-
s.project_id, s.linear_issue_id, s.issue_key, i.title,
|
|
365
|
-
i.current_linear_state, i.factory_state, s.session_state, s.waiting_reason, s.summary_text, s.updated_at,
|
|
366
|
-
i.pending_run_type,
|
|
367
|
-
i.pr_number, i.pr_head_sha, i.pr_review_state, i.pr_check_status, i.last_blocking_review_head_sha,
|
|
368
|
-
i.last_github_ci_snapshot_json,
|
|
369
|
-
i.last_github_failure_source,
|
|
370
|
-
i.last_github_failure_head_sha,
|
|
371
|
-
i.last_github_failure_check_name,
|
|
372
|
-
i.last_github_failure_context_json,
|
|
373
|
-
active_run.run_type AS active_run_type,
|
|
374
|
-
active_run.completion_check_thread_id AS active_completion_check_thread_id,
|
|
375
|
-
active_run.completion_check_outcome AS active_completion_check_outcome,
|
|
376
|
-
latest_run.run_type AS latest_run_type,
|
|
377
|
-
latest_run.status AS latest_run_status,
|
|
378
|
-
latest_run.summary_json AS latest_run_summary_json,
|
|
379
|
-
latest_run.report_json AS latest_run_report_json,
|
|
380
|
-
latest_run.completion_check_thread_id AS latest_run_completion_check_thread_id,
|
|
381
|
-
latest_run.completion_check_outcome AS latest_run_completion_check_outcome,
|
|
382
|
-
latest_run.completion_check_summary AS latest_run_completion_check_summary,
|
|
383
|
-
latest_run.completion_check_question AS latest_run_completion_check_question,
|
|
384
|
-
latest_run.completion_check_why AS latest_run_completion_check_why,
|
|
385
|
-
latest_run.completion_check_recommended_reply AS latest_run_completion_check_recommended_reply,
|
|
386
|
-
(
|
|
387
|
-
SELECT COUNT(*)
|
|
388
|
-
FROM issue_session_events e
|
|
389
|
-
WHERE e.project_id = s.project_id
|
|
390
|
-
AND e.linear_issue_id = s.linear_issue_id
|
|
391
|
-
AND e.processed_at IS NULL
|
|
392
|
-
) AS pending_session_event_count,
|
|
393
|
-
(
|
|
394
|
-
SELECT COUNT(*)
|
|
395
|
-
FROM issue_dependencies d
|
|
396
|
-
LEFT JOIN issues blockers
|
|
397
|
-
ON blockers.project_id = d.project_id
|
|
398
|
-
AND blockers.linear_issue_id = d.blocker_linear_issue_id
|
|
399
|
-
WHERE d.project_id = s.project_id
|
|
400
|
-
AND d.linear_issue_id = s.linear_issue_id
|
|
401
|
-
AND (
|
|
402
|
-
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
|
|
403
|
-
AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
|
|
404
|
-
)
|
|
405
|
-
) AS blocked_by_count,
|
|
406
|
-
(
|
|
407
|
-
SELECT json_group_array(COALESCE(blockers.issue_key, d.blocker_issue_key, d.blocker_linear_issue_id))
|
|
408
|
-
FROM issue_dependencies d
|
|
409
|
-
LEFT JOIN issues blockers
|
|
410
|
-
ON blockers.project_id = d.project_id
|
|
411
|
-
AND blockers.linear_issue_id = d.blocker_linear_issue_id
|
|
412
|
-
WHERE d.project_id = s.project_id
|
|
413
|
-
AND d.linear_issue_id = s.linear_issue_id
|
|
414
|
-
AND (
|
|
415
|
-
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
|
|
416
|
-
AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
|
|
417
|
-
)
|
|
418
|
-
) AS blocked_by_keys_json
|
|
419
|
-
FROM issue_sessions s
|
|
420
|
-
LEFT JOIN issues i
|
|
421
|
-
ON i.project_id = s.project_id
|
|
422
|
-
AND i.linear_issue_id = s.linear_issue_id
|
|
423
|
-
LEFT JOIN runs active_run ON active_run.id = COALESCE(s.active_run_id, i.active_run_id)
|
|
424
|
-
LEFT JOIN runs latest_run ON latest_run.id = (
|
|
425
|
-
SELECT r.id FROM runs r
|
|
426
|
-
WHERE r.project_id = s.project_id AND r.linear_issue_id = s.linear_issue_id
|
|
427
|
-
ORDER BY r.id DESC LIMIT 1
|
|
428
|
-
)
|
|
429
|
-
ORDER BY s.updated_at DESC, s.issue_key ASC`)
|
|
430
|
-
.all();
|
|
431
|
-
return rows.map((row) => {
|
|
432
|
-
const failureContext = parseGitHubFailureContext(typeof row.last_github_failure_context_json === "string" ? row.last_github_failure_context_json : undefined);
|
|
433
|
-
const prChecksSummary = parseCiSnapshotSummary(typeof row.last_github_ci_snapshot_json === "string" ? row.last_github_ci_snapshot_json : undefined);
|
|
434
|
-
const blockedByKeys = parseStringArray(typeof row.blocked_by_keys_json === "string" ? row.blocked_by_keys_json : undefined);
|
|
435
|
-
const blockedByCount = Number(row.blocked_by_count ?? 0);
|
|
436
|
-
const hasPendingSessionEvents = Number(row.pending_session_event_count ?? 0) > 0;
|
|
437
|
-
const hasPendingWake = hasPendingSessionEvents
|
|
438
|
-
|| this.db.issueSessions.peekIssueSessionWake(String(row.project_id), String(row.linear_issue_id)) !== undefined;
|
|
439
|
-
const readyForExecution = isIssueSessionReadyForExecution({
|
|
440
|
-
...(typeof row.session_state === "string" ? { sessionState: String(row.session_state) } : {}),
|
|
441
|
-
factoryState: String(row.factory_state ?? "delegated"),
|
|
442
|
-
...(row.active_run_type !== null ? { activeRunId: 1 } : {}),
|
|
443
|
-
blockedByCount,
|
|
444
|
-
hasPendingWake,
|
|
445
|
-
hasLegacyPendingRun: row.pending_run_type !== null && row.pending_run_type !== undefined,
|
|
446
|
-
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
447
|
-
...(row.pr_state !== null ? { prState: String(row.pr_state) } : {}),
|
|
448
|
-
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
449
|
-
...(row.pr_check_status !== null ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
450
|
-
...(row.last_github_failure_source !== null ? { latestFailureSource: String(row.last_github_failure_source) } : {}),
|
|
451
|
-
});
|
|
452
|
-
const failureSummary = summarizeGitHubFailureContext(failureContext);
|
|
453
|
-
const sessionWaitingReason = typeof row.waiting_reason === "string" && row.waiting_reason.trim().length > 0
|
|
454
|
-
? row.waiting_reason
|
|
455
|
-
: undefined;
|
|
456
|
-
const sessionSummary = typeof row.summary_text === "string" && row.summary_text.trim().length > 0
|
|
457
|
-
? row.summary_text
|
|
458
|
-
: undefined;
|
|
459
|
-
const waitingReason = sessionWaitingReason ?? derivePatchRelayWaitingReason({
|
|
460
|
-
...(row.active_run_type !== null ? { activeRunType: String(row.active_run_type) } : {}),
|
|
461
|
-
blockedByKeys,
|
|
462
|
-
factoryState: String(row.factory_state ?? "delegated"),
|
|
463
|
-
...(row.pending_run_type !== null ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
464
|
-
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
465
|
-
...(row.pr_head_sha !== null ? { prHeadSha: String(row.pr_head_sha) } : {}),
|
|
466
|
-
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
467
|
-
...(row.pr_check_status !== null ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
468
|
-
...(row.last_blocking_review_head_sha !== null ? { lastBlockingReviewHeadSha: String(row.last_blocking_review_head_sha) } : {}),
|
|
469
|
-
...(row.last_github_failure_check_name !== null ? { latestFailureCheckName: String(row.last_github_failure_check_name) } : {}),
|
|
470
|
-
});
|
|
471
|
-
const latestRun = row.latest_run_type !== null && row.latest_run_status !== null
|
|
472
|
-
? {
|
|
473
|
-
id: 0,
|
|
474
|
-
issueId: 0,
|
|
475
|
-
projectId: String(row.project_id),
|
|
476
|
-
linearIssueId: String(row.linear_issue_id),
|
|
477
|
-
runType: String(row.latest_run_type),
|
|
478
|
-
status: String(row.latest_run_status),
|
|
479
|
-
...(typeof row.latest_run_summary_json === "string" ? { summaryJson: row.latest_run_summary_json } : {}),
|
|
480
|
-
...(typeof row.latest_run_report_json === "string" ? { reportJson: row.latest_run_report_json } : {}),
|
|
481
|
-
...(typeof row.latest_run_completion_check_thread_id === "string" ? { completionCheckThreadId: row.latest_run_completion_check_thread_id } : {}),
|
|
482
|
-
...(typeof row.latest_run_completion_check_outcome === "string" ? { completionCheckOutcome: row.latest_run_completion_check_outcome } : {}),
|
|
483
|
-
...(typeof row.latest_run_completion_check_summary === "string" ? { completionCheckSummary: row.latest_run_completion_check_summary } : {}),
|
|
484
|
-
...(typeof row.latest_run_completion_check_question === "string" ? { completionCheckQuestion: row.latest_run_completion_check_question } : {}),
|
|
485
|
-
...(typeof row.latest_run_completion_check_why === "string" ? { completionCheckWhy: row.latest_run_completion_check_why } : {}),
|
|
486
|
-
...(typeof row.latest_run_completion_check_recommended_reply === "string" ? { completionCheckRecommendedReply: row.latest_run_completion_check_recommended_reply } : {}),
|
|
487
|
-
startedAt: String(row.updated_at),
|
|
488
|
-
}
|
|
489
|
-
: undefined;
|
|
490
|
-
const latestEvent = this.db.issueSessions.listIssueSessionEvents(String(row.project_id), String(row.linear_issue_id), { limit: 1 }).at(-1);
|
|
491
|
-
const statusNoteCandidate = deriveIssueStatusNote({
|
|
492
|
-
issue: { factoryState: String(row.factory_state ?? "delegated") },
|
|
493
|
-
sessionSummary,
|
|
494
|
-
latestRun: latestRun,
|
|
495
|
-
latestEvent,
|
|
496
|
-
failureSummary,
|
|
497
|
-
blockedByKeys,
|
|
498
|
-
waitingReason,
|
|
499
|
-
}) ?? waitingReason;
|
|
500
|
-
const statusNoteForReturn = shouldSuppressStatusNote({
|
|
501
|
-
activeRunType: row.active_run_type,
|
|
502
|
-
sessionState: row.session_state,
|
|
503
|
-
statusNote: statusNoteCandidate,
|
|
504
|
-
})
|
|
505
|
-
? undefined
|
|
506
|
-
: statusNoteCandidate;
|
|
507
|
-
const completionCheckActive = typeof row.active_completion_check_thread_id === "string"
|
|
508
|
-
&& row.active_completion_check_thread_id.length > 0
|
|
509
|
-
&& row.active_completion_check_outcome === null
|
|
510
|
-
&& row.active_run_type !== null;
|
|
511
|
-
return {
|
|
512
|
-
...(row.issue_key !== null ? { issueKey: String(row.issue_key) } : {}),
|
|
513
|
-
...(row.title !== null ? { title: String(row.title) } : {}),
|
|
514
|
-
...(statusNoteForReturn ? { statusNote: statusNoteForReturn } : {}),
|
|
515
|
-
projectId: String(row.project_id),
|
|
516
|
-
...(row.session_state !== null ? { sessionState: String(row.session_state) } : {}),
|
|
517
|
-
factoryState: String(row.factory_state ?? "delegated"),
|
|
518
|
-
blockedByCount,
|
|
519
|
-
blockedByKeys,
|
|
520
|
-
readyForExecution,
|
|
521
|
-
...(row.current_linear_state !== null ? { currentLinearState: String(row.current_linear_state) } : {}),
|
|
522
|
-
...(row.active_run_type !== null ? { activeRunType: String(row.active_run_type) } : {}),
|
|
523
|
-
...(row.pending_run_type !== null ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
524
|
-
...(row.latest_run_type !== null ? { latestRunType: String(row.latest_run_type) } : {}),
|
|
525
|
-
...(row.latest_run_status !== null ? { latestRunStatus: String(row.latest_run_status) } : {}),
|
|
526
|
-
...(row.pr_number !== null ? { prNumber: Number(row.pr_number) } : {}),
|
|
527
|
-
...(row.pr_review_state !== null ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
528
|
-
...(row.pr_check_status !== null ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
529
|
-
...(prChecksSummary ? { prChecksSummary } : {}),
|
|
530
|
-
...(row.last_github_failure_source !== null ? { latestFailureSource: String(row.last_github_failure_source) } : {}),
|
|
531
|
-
...(row.last_github_failure_head_sha !== null ? { latestFailureHeadSha: String(row.last_github_failure_head_sha) } : {}),
|
|
532
|
-
...(row.last_github_failure_check_name !== null ? { latestFailureCheckName: String(row.last_github_failure_check_name) } : {}),
|
|
533
|
-
...(failureContext?.stepName ? { latestFailureStepName: failureContext.stepName } : {}),
|
|
534
|
-
...(failureContext?.summary ? { latestFailureSummary: failureContext.summary } : {}),
|
|
535
|
-
...(waitingReason ? { waitingReason } : {}),
|
|
536
|
-
...(completionCheckActive ? { completionCheckActive } : {}),
|
|
537
|
-
updatedAt: String(row.updated_at),
|
|
538
|
-
};
|
|
539
|
-
});
|
|
208
|
+
return this.trackedIssueListQuery.listTrackedIssues();
|
|
540
209
|
}
|
|
541
210
|
async promptIssue(issueKey, text, source = "watch") {
|
|
542
|
-
|
|
543
|
-
if (!issue)
|
|
544
|
-
return undefined;
|
|
545
|
-
// Publish to operator feed so all clients see the prompt
|
|
546
|
-
this.feed.publish({
|
|
547
|
-
level: "info",
|
|
548
|
-
kind: "comment",
|
|
549
|
-
issueKey: issue.issueKey,
|
|
550
|
-
projectId: issue.projectId,
|
|
551
|
-
stage: issue.factoryState,
|
|
552
|
-
status: "operator_prompt",
|
|
553
|
-
summary: `Operator prompt (${source})`,
|
|
554
|
-
detail: text.slice(0, 200),
|
|
555
|
-
});
|
|
556
|
-
// If no active run, queue as pending context for the next run
|
|
557
|
-
if (!issue.activeRunId) {
|
|
558
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
559
|
-
projectId: issue.projectId,
|
|
560
|
-
linearIssueId: issue.linearIssueId,
|
|
561
|
-
eventType: "operator_prompt",
|
|
562
|
-
eventJson: JSON.stringify({ text, source }),
|
|
563
|
-
});
|
|
564
|
-
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
565
|
-
return { delivered: false, queued: true };
|
|
566
|
-
}
|
|
567
|
-
const run = this.db.runs.getRunById(issue.activeRunId);
|
|
568
|
-
if (!run?.threadId || !run.turnId) {
|
|
569
|
-
return { error: "Active run has no thread or turn yet" };
|
|
570
|
-
}
|
|
571
|
-
try {
|
|
572
|
-
await this.codex.steerTurn({
|
|
573
|
-
threadId: run.threadId,
|
|
574
|
-
turnId: run.turnId,
|
|
575
|
-
input: `Operator prompt (${source}):\n\n${text}`,
|
|
576
|
-
});
|
|
577
|
-
return { delivered: true };
|
|
578
|
-
}
|
|
579
|
-
catch (error) {
|
|
580
|
-
// Turn may have completed between check and steer — queue for next run
|
|
581
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
582
|
-
this.logger.warn({ issueKey, error: msg }, "steerTurn failed, queuing prompt for next run");
|
|
583
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
584
|
-
projectId: issue.projectId,
|
|
585
|
-
linearIssueId: issue.linearIssueId,
|
|
586
|
-
eventType: "operator_prompt",
|
|
587
|
-
eventJson: JSON.stringify({ text, source }),
|
|
588
|
-
});
|
|
589
|
-
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
590
|
-
return { delivered: false, queued: true };
|
|
591
|
-
}
|
|
211
|
+
return await this.issueActions.promptIssue(issueKey, text, source);
|
|
592
212
|
}
|
|
593
213
|
async stopIssue(issueKey) {
|
|
594
|
-
|
|
595
|
-
if (!issue)
|
|
596
|
-
return undefined;
|
|
597
|
-
if (!issue.activeRunId)
|
|
598
|
-
return { error: "No active run to stop" };
|
|
599
|
-
const run = this.db.runs.getRunById(issue.activeRunId);
|
|
600
|
-
if (run?.threadId && run.turnId) {
|
|
601
|
-
try {
|
|
602
|
-
await this.codex.steerTurn({
|
|
603
|
-
threadId: run.threadId,
|
|
604
|
-
turnId: run.turnId,
|
|
605
|
-
input: "STOP: The operator has requested this run to halt immediately. Finish your current action, commit any partial progress, and stop.",
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
catch {
|
|
609
|
-
// Turn may already be done
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
613
|
-
projectId: issue.projectId,
|
|
614
|
-
linearIssueId: issue.linearIssueId,
|
|
615
|
-
eventType: "stop_requested",
|
|
616
|
-
dedupeKey: `operator_stop:${issue.linearIssueId}`,
|
|
617
|
-
});
|
|
618
|
-
this.db.issueSessions.clearPendingIssueSessionEventsRespectingActiveLease(issue.projectId, issue.linearIssueId);
|
|
619
|
-
this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
620
|
-
projectId: issue.projectId,
|
|
621
|
-
linearIssueId: issue.linearIssueId,
|
|
622
|
-
factoryState: "awaiting_input",
|
|
623
|
-
});
|
|
624
|
-
this.feed.publish({
|
|
625
|
-
level: "warn",
|
|
626
|
-
kind: "workflow",
|
|
627
|
-
issueKey: issue.issueKey,
|
|
628
|
-
projectId: issue.projectId,
|
|
629
|
-
status: "stopped",
|
|
630
|
-
summary: "Operator stopped the run",
|
|
631
|
-
});
|
|
632
|
-
return { stopped: true };
|
|
214
|
+
return await this.issueActions.stopIssue(issueKey);
|
|
633
215
|
}
|
|
634
216
|
retryIssue(issueKey) {
|
|
635
|
-
|
|
636
|
-
if (!issue)
|
|
637
|
-
return undefined;
|
|
638
|
-
if (issue.activeRunId)
|
|
639
|
-
return { error: "Issue already has an active run" };
|
|
640
|
-
const issueSession = this.db.issueSessions.getIssueSession(issue.projectId, issue.linearIssueId);
|
|
641
|
-
if (issue.prState === "merged") {
|
|
642
|
-
this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
643
|
-
projectId: issue.projectId,
|
|
644
|
-
linearIssueId: issue.linearIssueId,
|
|
645
|
-
factoryState: "done",
|
|
646
|
-
});
|
|
647
|
-
return { issueKey, runType: "none" };
|
|
648
|
-
}
|
|
649
|
-
// Infer run type from current state instead of always resetting to implementation
|
|
650
|
-
let runType = "implementation";
|
|
651
|
-
let factoryState = "delegated";
|
|
652
|
-
if (issue.prNumber && issue.lastGitHubFailureSource === "queue_eviction") {
|
|
653
|
-
runType = "queue_repair";
|
|
654
|
-
factoryState = "repairing_queue";
|
|
655
|
-
}
|
|
656
|
-
else if (issue.prNumber && (issue.prCheckStatus === "failed" || issue.prCheckStatus === "failure" || issue.lastGitHubFailureSource === "branch_ci")) {
|
|
657
|
-
runType = "ci_repair";
|
|
658
|
-
factoryState = "repairing_ci";
|
|
659
|
-
}
|
|
660
|
-
else if (issue.prNumber && issue.prReviewState === "changes_requested") {
|
|
661
|
-
runType = issue.pendingRunType === "branch_upkeep" || issueSession?.lastRunType === "branch_upkeep"
|
|
662
|
-
? "branch_upkeep"
|
|
663
|
-
: "review_fix";
|
|
664
|
-
factoryState = "changes_requested";
|
|
665
|
-
}
|
|
666
|
-
else if (issue.prNumber) {
|
|
667
|
-
// PR exists but no specific failure — re-run implementation
|
|
668
|
-
runType = "implementation";
|
|
669
|
-
factoryState = "implementing";
|
|
670
|
-
}
|
|
671
|
-
this.appendOperatorRetryEvent(issue, runType);
|
|
672
|
-
this.db.issueSessions.upsertIssueRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
673
|
-
projectId: issue.projectId,
|
|
674
|
-
linearIssueId: issue.linearIssueId,
|
|
675
|
-
factoryState: factoryState,
|
|
676
|
-
});
|
|
677
|
-
this.feed.publish({
|
|
678
|
-
level: "info",
|
|
679
|
-
kind: "stage",
|
|
680
|
-
issueKey: issue.issueKey,
|
|
681
|
-
projectId: issue.projectId,
|
|
682
|
-
stage: factoryState,
|
|
683
|
-
status: "retry",
|
|
684
|
-
summary: `Retry queued: ${runType}`,
|
|
685
|
-
});
|
|
686
|
-
if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
687
|
-
this.runtime.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
688
|
-
}
|
|
689
|
-
return { issueKey, runType };
|
|
690
|
-
}
|
|
691
|
-
appendOperatorRetryEvent(issue, runType) {
|
|
692
|
-
if (runType === "queue_repair") {
|
|
693
|
-
const queueIncident = parseObjectJson(issue.lastQueueIncidentJson);
|
|
694
|
-
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson);
|
|
695
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
696
|
-
projectId: issue.projectId,
|
|
697
|
-
linearIssueId: issue.linearIssueId,
|
|
698
|
-
eventType: "merge_steward_incident",
|
|
699
|
-
eventJson: JSON.stringify({
|
|
700
|
-
...(queueIncident ?? {}),
|
|
701
|
-
...(failureContext ?? {}),
|
|
702
|
-
source: "operator_retry",
|
|
703
|
-
}),
|
|
704
|
-
dedupeKey: `operator_retry:queue_repair:${issue.linearIssueId}:${issue.prHeadSha ?? issue.lastGitHubFailureHeadSha ?? "unknown-sha"}`,
|
|
705
|
-
});
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
if (runType === "ci_repair") {
|
|
709
|
-
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson);
|
|
710
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
711
|
-
projectId: issue.projectId,
|
|
712
|
-
linearIssueId: issue.linearIssueId,
|
|
713
|
-
eventType: "settled_red_ci",
|
|
714
|
-
eventJson: JSON.stringify({
|
|
715
|
-
...(failureContext ?? {}),
|
|
716
|
-
source: "operator_retry",
|
|
717
|
-
}),
|
|
718
|
-
dedupeKey: `operator_retry:ci_repair:${issue.linearIssueId}:${issue.lastGitHubFailureSignature ?? issue.prHeadSha ?? "unknown-sha"}`,
|
|
719
|
-
});
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
if (runType === "review_fix" || runType === "branch_upkeep") {
|
|
723
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
724
|
-
projectId: issue.projectId,
|
|
725
|
-
linearIssueId: issue.linearIssueId,
|
|
726
|
-
eventType: "review_changes_requested",
|
|
727
|
-
eventJson: JSON.stringify({
|
|
728
|
-
reviewBody: runType === "branch_upkeep"
|
|
729
|
-
? "Operator requested retry of branch upkeep after requested changes."
|
|
730
|
-
: "Operator requested retry of review-fix work.",
|
|
731
|
-
...(runType === "branch_upkeep" ? { branchUpkeepRequired: true, wakeReason: "branch_upkeep" } : {}),
|
|
732
|
-
source: "operator_retry",
|
|
733
|
-
}),
|
|
734
|
-
dedupeKey: `operator_retry:${runType}:${issue.linearIssueId}:${issue.prHeadSha ?? "unknown-sha"}`,
|
|
735
|
-
});
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
739
|
-
projectId: issue.projectId,
|
|
740
|
-
linearIssueId: issue.linearIssueId,
|
|
741
|
-
eventType: "delegated",
|
|
742
|
-
eventJson: JSON.stringify({
|
|
743
|
-
promptContext: "Operator requested retry of PatchRelay work.",
|
|
744
|
-
source: "operator_retry",
|
|
745
|
-
}),
|
|
746
|
-
dedupeKey: `operator_retry:implementation:${issue.linearIssueId}`,
|
|
747
|
-
});
|
|
217
|
+
return this.issueActions.retryIssue(issueKey);
|
|
748
218
|
}
|
|
749
219
|
async acceptWebhook(params) {
|
|
750
220
|
const result = await acceptIncomingWebhook({
|
|
@@ -838,17 +308,6 @@ function toLinearClientProvider(linear) {
|
|
|
838
308
|
},
|
|
839
309
|
};
|
|
840
310
|
}
|
|
841
|
-
function parseStringArray(value) {
|
|
842
|
-
if (!value)
|
|
843
|
-
return [];
|
|
844
|
-
try {
|
|
845
|
-
const parsed = JSON.parse(value);
|
|
846
|
-
return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : [];
|
|
847
|
-
}
|
|
848
|
-
catch {
|
|
849
|
-
return [];
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
311
|
function workspaceMatches(workspace, installation) {
|
|
853
312
|
const normalized = workspace.trim().toLowerCase();
|
|
854
313
|
return [
|