patchrelay 0.36.7 → 0.36.9
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/cli/cluster-health.js +2 -2
- package/dist/cli/data.js +20 -20
- package/dist/db/issue-session-store.js +284 -0
- package/dist/db/issue-store.js +559 -0
- package/dist/db/run-store.js +125 -0
- package/dist/db/webhook-event-store.js +71 -0
- package/dist/db.js +52 -1138
- package/dist/github-webhook-handler.js +44 -44
- package/dist/idle-reconciliation.js +20 -20
- package/dist/interrupted-run-recovery.js +176 -0
- package/dist/issue-query-service.js +13 -13
- package/dist/issue-session-lease-service.js +143 -0
- package/dist/issue-session-projector.js +114 -0
- package/dist/linear-session-sync.js +10 -10
- package/dist/queue-health-monitor.js +5 -5
- package/dist/run-completion-policy.js +412 -0
- package/dist/run-finalizer.js +172 -0
- package/dist/run-launcher.js +193 -0
- package/dist/run-orchestrator.js +145 -1505
- package/dist/run-recovery-service.js +209 -0
- package/dist/run-wake-planner.js +101 -0
- package/dist/service.js +33 -33
- package/dist/tracked-issue-projector.js +69 -0
- package/dist/webhook-handler.js +64 -693
- package/dist/webhooks/agent-session-handler.js +212 -0
- package/dist/webhooks/comment-policy.js +41 -0
- package/dist/webhooks/comment-wake-handler.js +133 -0
- package/dist/webhooks/decision-helpers.js +74 -0
- package/dist/webhooks/desired-stage-recorder.js +177 -0
- package/dist/webhooks/issue-removal-handler.js +68 -0
- package/dist/worktree-manager.js +69 -0
- package/package.json +1 -1
|
@@ -29,7 +29,7 @@ export class IssueQueryService {
|
|
|
29
29
|
return await this.codex.readThread(run.threadId, true).catch(() => undefined);
|
|
30
30
|
}
|
|
31
31
|
buildRuns(projectId, linearIssueId) {
|
|
32
|
-
return this.db.listRunsForIssue(projectId, linearIssueId).map((run) => ({
|
|
32
|
+
return this.db.runs.listRunsForIssue(projectId, linearIssueId).map((run) => ({
|
|
33
33
|
id: run.id,
|
|
34
34
|
runType: run.runType,
|
|
35
35
|
status: run.status,
|
|
@@ -41,7 +41,7 @@ export class IssueQueryService {
|
|
|
41
41
|
return report ? { report } : {};
|
|
42
42
|
})(),
|
|
43
43
|
...(() => {
|
|
44
|
-
const events = this.db.listThreadEvents(run.id).flatMap((event) => {
|
|
44
|
+
const events = this.db.runs.listThreadEvents(run.id).flatMap((event) => {
|
|
45
45
|
try {
|
|
46
46
|
const parsed = JSON.parse(event.eventJson);
|
|
47
47
|
return [{
|
|
@@ -66,16 +66,16 @@ export class IssueQueryService {
|
|
|
66
66
|
}));
|
|
67
67
|
}
|
|
68
68
|
async getIssueOverview(issueKey) {
|
|
69
|
-
const session = this.db.getIssueSessionByKey(issueKey);
|
|
69
|
+
const session = this.db.issueSessions.getIssueSessionByKey(issueKey);
|
|
70
70
|
if (!session) {
|
|
71
71
|
const legacy = this.db.getIssueOverview(issueKey);
|
|
72
72
|
if (!legacy)
|
|
73
73
|
return undefined;
|
|
74
|
-
const issueRecord = this.db.getIssueByKey(issueKey);
|
|
74
|
+
const issueRecord = this.db.issues.getIssueByKey(issueKey);
|
|
75
75
|
const activeStatus = await this.runStatusProvider.getActiveRunStatus(issueKey);
|
|
76
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);
|
|
77
|
+
const latestRun = this.db.runs.getLatestRunForIssue(legacy.issue.projectId, legacy.issue.linearIssueId);
|
|
78
|
+
const latestEvent = this.db.issueSessions.listIssueSessionEvents(legacy.issue.projectId, legacy.issue.linearIssueId, { limit: 1 }).at(-1);
|
|
79
79
|
const runs = this.buildRuns(legacy.issue.projectId, legacy.issue.linearIssueId);
|
|
80
80
|
const runCount = runs.length;
|
|
81
81
|
const liveThread = await this.readLiveThread(activeRun);
|
|
@@ -123,16 +123,16 @@ export class IssueQueryService {
|
|
|
123
123
|
: {}),
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
|
-
const issueRecord = this.db.getIssueByKey(issueKey);
|
|
127
|
-
const blockedBy = this.db.listIssueDependencies(session.projectId, session.linearIssueId);
|
|
126
|
+
const issueRecord = this.db.issues.getIssueByKey(issueKey);
|
|
127
|
+
const blockedBy = this.db.issues.listIssueDependencies(session.projectId, session.linearIssueId);
|
|
128
128
|
const unresolvedBlockedBy = blockedBy.filter((entry) => (entry.blockerCurrentLinearStateType !== "completed"
|
|
129
129
|
&& entry.blockerCurrentLinearState?.trim().toLowerCase() !== "done"));
|
|
130
130
|
const blockedByKeys = unresolvedBlockedBy.map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId);
|
|
131
131
|
const activeStatus = await this.runStatusProvider.getActiveRunStatus(issueKey);
|
|
132
132
|
const activeRun = activeStatus?.run
|
|
133
|
-
?? (session.activeRunId !== undefined ? this.db.
|
|
134
|
-
const latestRun = this.db.getLatestRunForIssue(session.projectId, session.linearIssueId);
|
|
135
|
-
const latestEvent = this.db.listIssueSessionEvents(session.projectId, session.linearIssueId, { limit: 1 }).at(-1);
|
|
133
|
+
?? (session.activeRunId !== undefined ? this.db.runs.getRunById(session.activeRunId) : undefined);
|
|
134
|
+
const latestRun = this.db.runs.getLatestRunForIssue(session.projectId, session.linearIssueId);
|
|
135
|
+
const latestEvent = this.db.issueSessions.listIssueSessionEvents(session.projectId, session.linearIssueId, { limit: 1 }).at(-1);
|
|
136
136
|
const runs = this.buildRuns(session.projectId, session.linearIssueId);
|
|
137
137
|
const runCount = runs.length;
|
|
138
138
|
const liveThread = await this.readLiveThread(activeRun);
|
|
@@ -166,7 +166,7 @@ export class IssueQueryService {
|
|
|
166
166
|
factoryState: issueRecord?.factoryState ?? "delegated",
|
|
167
167
|
...(activeRun ? { activeRunId: activeRun.id } : {}),
|
|
168
168
|
blockedByCount: unresolvedBlockedBy.length,
|
|
169
|
-
hasPendingWake: this.db.peekIssueSessionWake(session.projectId, session.linearIssueId) !== undefined,
|
|
169
|
+
hasPendingWake: this.db.issueSessions.peekIssueSessionWake(session.projectId, session.linearIssueId) !== undefined,
|
|
170
170
|
hasLegacyPendingRun: issueRecord?.pendingRunType !== undefined,
|
|
171
171
|
...(session.prNumber !== undefined ? { prNumber: session.prNumber } : {}),
|
|
172
172
|
...(issueRecord?.prState ? { prState: issueRecord.prState } : {}),
|
|
@@ -231,7 +231,7 @@ export class IssueQueryService {
|
|
|
231
231
|
const overview = await this.getIssueOverview(issueKey);
|
|
232
232
|
if (!overview)
|
|
233
233
|
return undefined;
|
|
234
|
-
const issueRecord = this.db.getIssueByKey(issueKey);
|
|
234
|
+
const issueRecord = this.db.issues.getIssueByKey(issueKey);
|
|
235
235
|
const latestRunReport = parseStageReport(overview.latestRun?.reportJson, overview.latestRun?.status ?? "unknown");
|
|
236
236
|
const runs = (overview.runs ?? this.buildRuns(overview.issue.projectId, overview.issue.linearIssueId)).map((run) => ({
|
|
237
237
|
run: {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getThreadTurns } from "./codex-thread-utils.js";
|
|
3
|
+
const ISSUE_SESSION_LEASE_MS = 10 * 60_000;
|
|
4
|
+
export class IssueSessionLeaseService {
|
|
5
|
+
db;
|
|
6
|
+
logger;
|
|
7
|
+
workerId;
|
|
8
|
+
readThreadWithRetry;
|
|
9
|
+
activeSessionLeases = new Map();
|
|
10
|
+
constructor(db, logger, workerId, readThreadWithRetry) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
this.workerId = workerId;
|
|
14
|
+
this.readThreadWithRetry = readThreadWithRetry;
|
|
15
|
+
}
|
|
16
|
+
hasLocalLease(projectId, linearIssueId) {
|
|
17
|
+
return this.activeSessionLeases.has(this.issueSessionLeaseKey(projectId, linearIssueId));
|
|
18
|
+
}
|
|
19
|
+
getHeldLease(projectId, linearIssueId) {
|
|
20
|
+
const leaseId = this.activeSessionLeases.get(this.issueSessionLeaseKey(projectId, linearIssueId));
|
|
21
|
+
if (!leaseId)
|
|
22
|
+
return undefined;
|
|
23
|
+
return { projectId, linearIssueId, leaseId };
|
|
24
|
+
}
|
|
25
|
+
withHeldLease(projectId, linearIssueId, fn) {
|
|
26
|
+
const lease = this.getHeldLease(projectId, linearIssueId);
|
|
27
|
+
if (!lease)
|
|
28
|
+
return undefined;
|
|
29
|
+
return this.db.issueSessions.withIssueSessionLease(projectId, linearIssueId, lease.leaseId, () => fn(lease));
|
|
30
|
+
}
|
|
31
|
+
acquire(projectId, linearIssueId) {
|
|
32
|
+
const leaseId = randomUUID();
|
|
33
|
+
const leasedUntil = new Date(Date.now() + ISSUE_SESSION_LEASE_MS).toISOString();
|
|
34
|
+
const acquired = this.db.issueSessions.acquireIssueSessionLease({
|
|
35
|
+
projectId,
|
|
36
|
+
linearIssueId,
|
|
37
|
+
leaseId,
|
|
38
|
+
workerId: this.workerId,
|
|
39
|
+
leasedUntil,
|
|
40
|
+
});
|
|
41
|
+
if (!acquired)
|
|
42
|
+
return undefined;
|
|
43
|
+
this.activeSessionLeases.set(this.issueSessionLeaseKey(projectId, linearIssueId), leaseId);
|
|
44
|
+
return leaseId;
|
|
45
|
+
}
|
|
46
|
+
forceAcquire(projectId, linearIssueId) {
|
|
47
|
+
const leaseId = randomUUID();
|
|
48
|
+
const leasedUntil = new Date(Date.now() + ISSUE_SESSION_LEASE_MS).toISOString();
|
|
49
|
+
const acquired = this.db.issueSessions.forceAcquireIssueSessionLease({
|
|
50
|
+
projectId,
|
|
51
|
+
linearIssueId,
|
|
52
|
+
leaseId,
|
|
53
|
+
workerId: this.workerId,
|
|
54
|
+
leasedUntil,
|
|
55
|
+
});
|
|
56
|
+
if (!acquired)
|
|
57
|
+
return undefined;
|
|
58
|
+
this.activeSessionLeases.set(this.issueSessionLeaseKey(projectId, linearIssueId), leaseId);
|
|
59
|
+
return leaseId;
|
|
60
|
+
}
|
|
61
|
+
claimForReconciliation(projectId, linearIssueId) {
|
|
62
|
+
const key = this.issueSessionLeaseKey(projectId, linearIssueId);
|
|
63
|
+
if (this.activeSessionLeases.has(key)) {
|
|
64
|
+
return "owned";
|
|
65
|
+
}
|
|
66
|
+
const session = this.db.issueSessions.getIssueSession(projectId, linearIssueId);
|
|
67
|
+
if (!session)
|
|
68
|
+
return "skip";
|
|
69
|
+
const leasedUntilMs = session.leasedUntil ? Date.parse(session.leasedUntil) : undefined;
|
|
70
|
+
if (leasedUntilMs !== undefined && Number.isFinite(leasedUntilMs) && leasedUntilMs > Date.now()) {
|
|
71
|
+
return "skip";
|
|
72
|
+
}
|
|
73
|
+
return this.acquire(projectId, linearIssueId) ? true : "skip";
|
|
74
|
+
}
|
|
75
|
+
async reclaimForeignRecoveryLeaseIfSafe(run, issue) {
|
|
76
|
+
const key = this.issueSessionLeaseKey(run.projectId, run.linearIssueId);
|
|
77
|
+
if (this.activeSessionLeases.has(key)) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const session = this.db.issueSessions.getIssueSession(run.projectId, run.linearIssueId);
|
|
81
|
+
if (!session?.leaseId || !session.workerId || session.workerId === this.workerId) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (issue.activeRunId !== run.id) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
let safeToReclaim = !run.threadId;
|
|
88
|
+
if (!safeToReclaim && run.threadId) {
|
|
89
|
+
try {
|
|
90
|
+
const thread = await this.readThreadWithRetry(run.threadId, 1);
|
|
91
|
+
const latestTurn = getThreadTurns(thread).at(-1);
|
|
92
|
+
safeToReclaim = thread.status === "notLoaded"
|
|
93
|
+
|| latestTurn?.status === "interrupted"
|
|
94
|
+
|| latestTurn?.status === "completed";
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
safeToReclaim = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!safeToReclaim) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const leaseId = this.forceAcquire(run.projectId, run.linearIssueId);
|
|
104
|
+
if (!leaseId) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
this.logger.info({
|
|
108
|
+
issueKey: issue.issueKey,
|
|
109
|
+
runId: run.id,
|
|
110
|
+
previousWorkerId: session.workerId,
|
|
111
|
+
previousLeaseId: session.leaseId,
|
|
112
|
+
reclaimedLeaseId: leaseId,
|
|
113
|
+
}, "Reclaimed foreign issue-session lease for active-run recovery");
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
heartbeat(projectId, linearIssueId) {
|
|
117
|
+
const key = this.issueSessionLeaseKey(projectId, linearIssueId);
|
|
118
|
+
const leaseId = this.activeSessionLeases.get(key) ?? this.db.issueSessions.getIssueSession(projectId, linearIssueId)?.leaseId;
|
|
119
|
+
if (!leaseId)
|
|
120
|
+
return false;
|
|
121
|
+
const renewed = this.db.issueSessions.renewIssueSessionLease({
|
|
122
|
+
projectId,
|
|
123
|
+
linearIssueId,
|
|
124
|
+
leaseId,
|
|
125
|
+
leasedUntil: new Date(Date.now() + ISSUE_SESSION_LEASE_MS).toISOString(),
|
|
126
|
+
});
|
|
127
|
+
if (renewed) {
|
|
128
|
+
this.activeSessionLeases.set(key, leaseId);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
this.activeSessionLeases.delete(key);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
release(projectId, linearIssueId) {
|
|
135
|
+
const key = this.issueSessionLeaseKey(projectId, linearIssueId);
|
|
136
|
+
const leaseId = this.activeSessionLeases.get(key);
|
|
137
|
+
this.db.issueSessions.releaseIssueSessionLease(projectId, linearIssueId, leaseId);
|
|
138
|
+
this.activeSessionLeases.delete(key);
|
|
139
|
+
}
|
|
140
|
+
issueSessionLeaseKey(projectId, linearIssueId) {
|
|
141
|
+
return `${projectId}:${linearIssueId}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { isoNow } from "./db/shared.js";
|
|
2
|
+
import { buildTrackedIssueRecord } from "./tracked-issue-projector.js";
|
|
3
|
+
import { extractLatestAssistantSummary, } from "./issue-session-events.js";
|
|
4
|
+
import { deriveIssueSessionState, deriveIssueSessionWakeReason, } from "./issue-session.js";
|
|
5
|
+
export function syncIssueSessionFromIssue(params) {
|
|
6
|
+
const { connection, issues, issueSessions, runs, issue, options } = params;
|
|
7
|
+
const existing = issueSessions.getIssueSession(issue.projectId, issue.linearIssueId);
|
|
8
|
+
const latestRun = runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
9
|
+
const latestRunType = options?.lastRunType ?? latestRun?.runType ?? existing?.lastRunType;
|
|
10
|
+
const summaryText = resolveIssueSessionSummary(issue, runs, latestRun, existing?.summaryText, options?.summaryText);
|
|
11
|
+
const activeThreadId = issue.threadId ?? existing?.activeThreadId;
|
|
12
|
+
const threadGeneration = activeThreadId && activeThreadId !== existing?.activeThreadId
|
|
13
|
+
? (existing?.threadGeneration ?? 0) + 1
|
|
14
|
+
: (existing?.threadGeneration ?? (activeThreadId ? 1 : 0));
|
|
15
|
+
const sessionState = deriveIssueSessionState({
|
|
16
|
+
...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
|
|
17
|
+
factoryState: issue.factoryState,
|
|
18
|
+
});
|
|
19
|
+
const tracked = buildTrackedIssueRecord({
|
|
20
|
+
issue,
|
|
21
|
+
session: existing,
|
|
22
|
+
blockedBy: issues.listIssueDependencies(issue.projectId, issue.linearIssueId),
|
|
23
|
+
hasPendingWake: issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined,
|
|
24
|
+
latestRun,
|
|
25
|
+
latestEvent: issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1),
|
|
26
|
+
});
|
|
27
|
+
const lastWakeReason = options?.lastWakeReason
|
|
28
|
+
?? deriveIssueSessionWakeReason({
|
|
29
|
+
pendingRunType: issue.pendingRunType,
|
|
30
|
+
factoryState: issue.factoryState,
|
|
31
|
+
prNumber: issue.prNumber,
|
|
32
|
+
prState: issue.prState,
|
|
33
|
+
prReviewState: issue.prReviewState,
|
|
34
|
+
prCheckStatus: issue.prCheckStatus,
|
|
35
|
+
latestFailureSource: issue.lastGitHubFailureSource,
|
|
36
|
+
})
|
|
37
|
+
?? existing?.lastWakeReason;
|
|
38
|
+
const now = isoNow();
|
|
39
|
+
if (existing) {
|
|
40
|
+
connection.prepare(`
|
|
41
|
+
UPDATE issue_sessions SET
|
|
42
|
+
issue_key = ?,
|
|
43
|
+
repo_id = ?,
|
|
44
|
+
branch_name = ?,
|
|
45
|
+
worktree_path = ?,
|
|
46
|
+
pr_number = ?,
|
|
47
|
+
pr_head_sha = ?,
|
|
48
|
+
pr_author_login = ?,
|
|
49
|
+
session_state = ?,
|
|
50
|
+
waiting_reason = ?,
|
|
51
|
+
summary_text = ?,
|
|
52
|
+
active_thread_id = ?,
|
|
53
|
+
thread_generation = ?,
|
|
54
|
+
active_run_id = ?,
|
|
55
|
+
last_run_type = ?,
|
|
56
|
+
last_wake_reason = ?,
|
|
57
|
+
ci_repair_attempts = ?,
|
|
58
|
+
queue_repair_attempts = ?,
|
|
59
|
+
review_fix_attempts = ?,
|
|
60
|
+
updated_at = ?
|
|
61
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
62
|
+
`).run(issue.issueKey ?? null, issue.projectId, issue.branchName ?? null, issue.worktreePath ?? null, issue.prNumber ?? null, issue.prHeadSha ?? null, issue.prAuthorLogin ?? null, sessionState, tracked.waitingReason ?? null, summaryText ?? null, activeThreadId ?? null, threadGeneration, issue.activeRunId ?? null, latestRunType ?? null, lastWakeReason ?? null, issue.ciRepairAttempts, issue.queueRepairAttempts, issue.reviewFixAttempts, now, issue.projectId, issue.linearIssueId);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
connection.prepare(`
|
|
66
|
+
INSERT INTO issue_sessions (
|
|
67
|
+
project_id, linear_issue_id, issue_key, repo_id, branch_name, worktree_path,
|
|
68
|
+
pr_number, pr_head_sha, pr_author_login, session_state, waiting_reason, summary_text,
|
|
69
|
+
active_thread_id, thread_generation, active_run_id, last_run_type, last_wake_reason,
|
|
70
|
+
ci_repair_attempts, queue_repair_attempts, review_fix_attempts,
|
|
71
|
+
created_at, updated_at
|
|
72
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
73
|
+
`).run(issue.projectId, issue.linearIssueId, issue.issueKey ?? null, issue.projectId, issue.branchName ?? null, issue.worktreePath ?? null, issue.prNumber ?? null, issue.prHeadSha ?? null, issue.prAuthorLogin ?? null, sessionState, tracked.waitingReason ?? null, summaryText ?? null, activeThreadId ?? null, threadGeneration, issue.activeRunId ?? null, latestRunType ?? null, lastWakeReason ?? null, issue.ciRepairAttempts, issue.queueRepairAttempts, issue.reviewFixAttempts, now, now);
|
|
74
|
+
}
|
|
75
|
+
function resolveIssueSessionSummary(issue, runs, latestRun, existingSummaryText, explicitSummaryText) {
|
|
76
|
+
if (explicitSummaryText?.trim()) {
|
|
77
|
+
return explicitSummaryText;
|
|
78
|
+
}
|
|
79
|
+
const latestSummary = extractLatestAssistantSummary(latestRun);
|
|
80
|
+
if (latestRun && (latestRun.status === "queued" || latestRun.status === "running")) {
|
|
81
|
+
return latestSummary;
|
|
82
|
+
}
|
|
83
|
+
if (shouldKeepPreviousIssueSummary(issue, latestRun)) {
|
|
84
|
+
return findLatestCompletedRunSummary(runs, issue.projectId, issue.linearIssueId)
|
|
85
|
+
?? existingSummaryText
|
|
86
|
+
?? latestSummary;
|
|
87
|
+
}
|
|
88
|
+
return latestSummary ?? existingSummaryText;
|
|
89
|
+
}
|
|
90
|
+
function shouldKeepPreviousIssueSummary(issue, latestRun) {
|
|
91
|
+
if (!latestRun || latestRun.status !== "failed") {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (latestRun.summaryJson || latestRun.reportJson) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return issue.factoryState === "pr_open"
|
|
98
|
+
|| issue.factoryState === "awaiting_queue"
|
|
99
|
+
|| issue.factoryState === "done";
|
|
100
|
+
}
|
|
101
|
+
function findLatestCompletedRunSummary(runs, projectId, linearIssueId) {
|
|
102
|
+
const issueRuns = runs.listRunsForIssue(projectId, linearIssueId);
|
|
103
|
+
for (let index = issueRuns.length - 1; index >= 0; index -= 1) {
|
|
104
|
+
const run = issueRuns[index];
|
|
105
|
+
if (!run || run.status !== "completed") {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const summary = extractLatestAssistantSummary(run);
|
|
109
|
+
if (summary?.trim()) {
|
|
110
|
+
return summary;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
@@ -22,11 +22,11 @@ export class LinearSessionSync {
|
|
|
22
22
|
if (issue.agentSessionId) {
|
|
23
23
|
return issue;
|
|
24
24
|
}
|
|
25
|
-
const recoveredAgentSessionId = this.db.findLatestAgentSessionIdForIssue(issue.linearIssueId);
|
|
25
|
+
const recoveredAgentSessionId = this.db.webhookEvents.findLatestAgentSessionIdForIssue(issue.linearIssueId);
|
|
26
26
|
if (!recoveredAgentSessionId)
|
|
27
27
|
return issue;
|
|
28
28
|
this.logger.info({ issueKey: issue.issueKey, agentSessionId: recoveredAgentSessionId }, "Recovered missing Linear agent session id from webhook history");
|
|
29
|
-
return this.db.upsertIssue({
|
|
29
|
+
return this.db.issues.upsertIssue({
|
|
30
30
|
projectId: issue.projectId,
|
|
31
31
|
linearIssueId: issue.linearIssueId,
|
|
32
32
|
agentSessionId: recoveredAgentSessionId,
|
|
@@ -99,7 +99,7 @@ export class LinearSessionSync {
|
|
|
99
99
|
currentLinearState: liveIssue.stateName,
|
|
100
100
|
currentLinearStateType: liveIssue.stateType,
|
|
101
101
|
})) {
|
|
102
|
-
this.db.upsertIssue({
|
|
102
|
+
this.db.issues.upsertIssue({
|
|
103
103
|
projectId: issue.projectId,
|
|
104
104
|
linearIssueId: issue.linearIssueId,
|
|
105
105
|
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
@@ -112,7 +112,7 @@ export class LinearSessionSync {
|
|
|
112
112
|
return;
|
|
113
113
|
const normalizedCurrent = liveIssue.stateName?.trim().toLowerCase();
|
|
114
114
|
if (normalizedCurrent === targetState.trim().toLowerCase()) {
|
|
115
|
-
this.db.upsertIssue({
|
|
115
|
+
this.db.issues.upsertIssue({
|
|
116
116
|
projectId: issue.projectId,
|
|
117
117
|
linearIssueId: issue.linearIssueId,
|
|
118
118
|
...(liveIssue.stateName ? { currentLinearState: liveIssue.stateName } : {}),
|
|
@@ -121,7 +121,7 @@ export class LinearSessionSync {
|
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
123
|
const updated = await linear.setIssueState(issue.linearIssueId, targetState);
|
|
124
|
-
this.db.upsertIssue({
|
|
124
|
+
this.db.issues.upsertIssue({
|
|
125
125
|
projectId: issue.projectId,
|
|
126
126
|
linearIssueId: issue.linearIssueId,
|
|
127
127
|
...(updated.stateName ? { currentLinearState: updated.stateName } : {}),
|
|
@@ -174,7 +174,7 @@ export class LinearSessionSync {
|
|
|
174
174
|
if (now - lastEmit < PROGRESS_THROTTLE_MS)
|
|
175
175
|
return;
|
|
176
176
|
this.progressThrottle.set(run.id, now);
|
|
177
|
-
const issue = this.db.getIssue(run.projectId, run.linearIssueId);
|
|
177
|
+
const issue = this.db.issues.getIssue(run.projectId, run.linearIssueId);
|
|
178
178
|
if (issue) {
|
|
179
179
|
void this.emitActivity(issue, activity, { ephemeral: true });
|
|
180
180
|
}
|
|
@@ -192,7 +192,7 @@ export class LinearSessionSync {
|
|
|
192
192
|
body,
|
|
193
193
|
});
|
|
194
194
|
if (result.id !== issue.statusCommentId) {
|
|
195
|
-
this.db.upsertIssue({
|
|
195
|
+
this.db.issues.upsertIssue({
|
|
196
196
|
projectId: issue.projectId,
|
|
197
197
|
linearIssueId: issue.linearIssueId,
|
|
198
198
|
statusCommentId: result.id,
|
|
@@ -229,9 +229,9 @@ function resolveProgressActivity(notification) {
|
|
|
229
229
|
return undefined;
|
|
230
230
|
}
|
|
231
231
|
function renderStatusComment(db, issue, trackedIssue, options) {
|
|
232
|
-
const activeRun = issue.activeRunId ? db.
|
|
233
|
-
const latestRun = db.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
234
|
-
const latestEvent = db.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1);
|
|
232
|
+
const activeRun = issue.activeRunId ? db.runs.getRunById(issue.activeRunId) : undefined;
|
|
233
|
+
const latestRun = db.runs.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
234
|
+
const latestEvent = db.issueSessions.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1);
|
|
235
235
|
const activeRunType = issue.activeRunId !== undefined
|
|
236
236
|
? (options?.activeRunType ?? activeRun?.runType)
|
|
237
237
|
: undefined;
|
|
@@ -25,7 +25,7 @@ export class QueueHealthMonitor {
|
|
|
25
25
|
this.feed = feed;
|
|
26
26
|
}
|
|
27
27
|
async reconcile() {
|
|
28
|
-
for (const issue of this.db.listAwaitingQueueIssues()) {
|
|
28
|
+
for (const issue of this.db.issues.listAwaitingQueueIssues()) {
|
|
29
29
|
await this.probeQueuedIssue(issue);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -68,7 +68,7 @@ export class QueueHealthMonitor {
|
|
|
68
68
|
}
|
|
69
69
|
this.probeFailureFeedTimes.delete(`${issue.projectId}::${issue.linearIssueId}`);
|
|
70
70
|
if (pr.state === "MERGED") {
|
|
71
|
-
this.db.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prState: "merged" });
|
|
71
|
+
this.db.issues.upsertIssue({ projectId: issue.projectId, linearIssueId: issue.linearIssueId, prState: "merged" });
|
|
72
72
|
this.advancer.advanceIdleIssue(issue, "done", { clearFailureProvenance: true });
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
@@ -101,13 +101,13 @@ export class QueueHealthMonitor {
|
|
|
101
101
|
if (isDuplicateProbe(issue, pendingRunContext)) {
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
|
-
this.db.upsertIssue({
|
|
104
|
+
this.db.issues.upsertIssue({
|
|
105
105
|
projectId: issue.projectId,
|
|
106
106
|
linearIssueId: issue.linearIssueId,
|
|
107
107
|
lastAttemptedFailureHeadSha: headRefOid,
|
|
108
108
|
lastAttemptedFailureSignature: signature,
|
|
109
109
|
});
|
|
110
|
-
this.db.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
110
|
+
this.db.issueSessions.appendIssueSessionEventRespectingActiveLease(issue.projectId, issue.linearIssueId, {
|
|
111
111
|
projectId: issue.projectId,
|
|
112
112
|
linearIssueId: issue.linearIssueId,
|
|
113
113
|
eventType: "merge_steward_incident",
|
|
@@ -115,7 +115,7 @@ export class QueueHealthMonitor {
|
|
|
115
115
|
dedupeKey: `queue_health:queue_repair:${issue.linearIssueId}:${signature}`,
|
|
116
116
|
});
|
|
117
117
|
this.advancer.advanceIdleIssue(issue, "repairing_queue");
|
|
118
|
-
if (this.db.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
118
|
+
if (this.db.issueSessions.peekIssueSessionWake(issue.projectId, issue.linearIssueId)) {
|
|
119
119
|
this.advancer.enqueueIssue(issue.projectId, issue.linearIssueId);
|
|
120
120
|
}
|
|
121
121
|
this.logger.info({ issueKey: issue.issueKey, prNumber: issue.prNumber, headRefOid, reason }, "Queue health: queue issue detected, dispatching repair");
|