patchrelay 0.36.6 → 0.36.8
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 +3 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/cluster-health.js +8 -8
- package/dist/cli/commands/setup.js +32 -27
- package/dist/cli/data.js +11 -11
- package/dist/cli/help.js +1 -1
- package/dist/cli/service-commands.js +11 -0
- package/dist/config.js +48 -0
- package/dist/db/issue-session-store.js +292 -0
- package/dist/db/run-store.js +127 -0
- package/dist/db/webhook-event-store.js +71 -0
- package/dist/db.js +22 -520
- package/dist/github-webhook-handler.js +25 -25
- package/dist/idle-reconciliation.js +5 -5
- package/dist/issue-query-service.js +9 -9
- package/dist/issue-session-lease-service.js +143 -0
- package/dist/linear-session-sync.js +4 -4
- package/dist/patchrelay-customization.js +68 -0
- package/dist/prompting/patchrelay.js +552 -0
- package/dist/queue-health-monitor.js +2 -2
- package/dist/run-finalizer.js +161 -0
- package/dist/run-launcher.js +193 -0
- package/dist/run-orchestrator.js +151 -1396
- package/dist/run-recovery-service.js +203 -0
- package/dist/run-wake-planner.js +101 -0
- package/dist/service.js +24 -24
- package/dist/tracked-issue-projector.js +69 -0
- package/dist/webhook-handler.js +59 -688
- 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/package.json +1 -1
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { deriveSessionWakePlan } from "../issue-session-events.js";
|
|
2
|
+
import { isoNow } from "./shared.js";
|
|
3
|
+
export class IssueSessionStore {
|
|
4
|
+
connection;
|
|
5
|
+
mapIssueSessionRow;
|
|
6
|
+
mapIssueSessionEventRow;
|
|
7
|
+
getIssue;
|
|
8
|
+
deriveImplicitReactiveWake;
|
|
9
|
+
transaction;
|
|
10
|
+
upsertIssue;
|
|
11
|
+
finishRun;
|
|
12
|
+
updateRunThread;
|
|
13
|
+
setBranchOwner;
|
|
14
|
+
constructor(connection, mapIssueSessionRow, mapIssueSessionEventRow, getIssue, deriveImplicitReactiveWake, transaction, upsertIssue, finishRun, updateRunThread, setBranchOwner) {
|
|
15
|
+
this.connection = connection;
|
|
16
|
+
this.mapIssueSessionRow = mapIssueSessionRow;
|
|
17
|
+
this.mapIssueSessionEventRow = mapIssueSessionEventRow;
|
|
18
|
+
this.getIssue = getIssue;
|
|
19
|
+
this.deriveImplicitReactiveWake = deriveImplicitReactiveWake;
|
|
20
|
+
this.transaction = transaction;
|
|
21
|
+
this.upsertIssue = upsertIssue;
|
|
22
|
+
this.finishRun = finishRun;
|
|
23
|
+
this.updateRunThread = updateRunThread;
|
|
24
|
+
this.setBranchOwner = setBranchOwner;
|
|
25
|
+
}
|
|
26
|
+
getIssueSession(projectId, linearIssueId) {
|
|
27
|
+
const row = this.connection
|
|
28
|
+
.prepare("SELECT * FROM issue_sessions WHERE project_id = ? AND linear_issue_id = ?")
|
|
29
|
+
.get(projectId, linearIssueId);
|
|
30
|
+
return row ? this.mapIssueSessionRow(row) : undefined;
|
|
31
|
+
}
|
|
32
|
+
getIssueSessionByKey(issueKey) {
|
|
33
|
+
const row = this.connection.prepare("SELECT * FROM issue_sessions WHERE issue_key = ?").get(issueKey);
|
|
34
|
+
return row ? this.mapIssueSessionRow(row) : undefined;
|
|
35
|
+
}
|
|
36
|
+
appendIssueSessionEvent(params) {
|
|
37
|
+
if (params.dedupeKey) {
|
|
38
|
+
const existing = this.connection.prepare(`
|
|
39
|
+
SELECT * FROM issue_session_events
|
|
40
|
+
WHERE project_id = ? AND linear_issue_id = ? AND dedupe_key = ? AND processed_at IS NULL
|
|
41
|
+
ORDER BY id DESC LIMIT 1
|
|
42
|
+
`).get(params.projectId, params.linearIssueId, params.dedupeKey);
|
|
43
|
+
if (existing)
|
|
44
|
+
return this.mapIssueSessionEventRow(existing);
|
|
45
|
+
}
|
|
46
|
+
const now = isoNow();
|
|
47
|
+
const result = this.connection.prepare(`
|
|
48
|
+
INSERT INTO issue_session_events (
|
|
49
|
+
project_id, linear_issue_id, event_type, event_json, dedupe_key, created_at
|
|
50
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
51
|
+
`).run(params.projectId, params.linearIssueId, params.eventType, params.eventJson ?? null, params.dedupeKey ?? null, now);
|
|
52
|
+
return this.getIssueSessionEvent(Number(result.lastInsertRowid));
|
|
53
|
+
}
|
|
54
|
+
appendIssueSessionEventWithLease(lease, params) {
|
|
55
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => this.appendIssueSessionEvent(params));
|
|
56
|
+
}
|
|
57
|
+
appendIssueSessionEventRespectingActiveLease(projectId, linearIssueId, params) {
|
|
58
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
59
|
+
if (!lease) {
|
|
60
|
+
return this.appendIssueSessionEvent(params);
|
|
61
|
+
}
|
|
62
|
+
return this.appendIssueSessionEventWithLease(lease, params);
|
|
63
|
+
}
|
|
64
|
+
getIssueSessionEvent(id) {
|
|
65
|
+
const row = this.connection.prepare("SELECT * FROM issue_session_events WHERE id = ?").get(id);
|
|
66
|
+
return row ? this.mapIssueSessionEventRow(row) : undefined;
|
|
67
|
+
}
|
|
68
|
+
listIssueSessionEvents(projectId, linearIssueId, options) {
|
|
69
|
+
const conditions = ["project_id = ?", "linear_issue_id = ?"];
|
|
70
|
+
const values = [projectId, linearIssueId];
|
|
71
|
+
if (options?.pendingOnly) {
|
|
72
|
+
conditions.push("processed_at IS NULL");
|
|
73
|
+
}
|
|
74
|
+
let query = `SELECT * FROM issue_session_events WHERE ${conditions.join(" AND ")} ORDER BY id`;
|
|
75
|
+
if (options?.limit !== undefined) {
|
|
76
|
+
query += " LIMIT ?";
|
|
77
|
+
values.push(options.limit);
|
|
78
|
+
}
|
|
79
|
+
const rows = this.connection.prepare(query).all(...values);
|
|
80
|
+
return rows.map(this.mapIssueSessionEventRow);
|
|
81
|
+
}
|
|
82
|
+
consumeIssueSessionEvents(projectId, linearIssueId, eventIds, runId) {
|
|
83
|
+
if (eventIds.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
const now = isoNow();
|
|
86
|
+
const placeholders = eventIds.map(() => "?").join(", ");
|
|
87
|
+
this.connection.prepare(`
|
|
88
|
+
UPDATE issue_session_events
|
|
89
|
+
SET processed_at = ?, consumed_by_run_id = ?
|
|
90
|
+
WHERE project_id = ? AND linear_issue_id = ? AND id IN (${placeholders}) AND processed_at IS NULL
|
|
91
|
+
`).run(now, runId, projectId, linearIssueId, ...eventIds);
|
|
92
|
+
}
|
|
93
|
+
clearPendingIssueSessionEvents(projectId, linearIssueId) {
|
|
94
|
+
this.connection.prepare(`
|
|
95
|
+
UPDATE issue_session_events
|
|
96
|
+
SET processed_at = ?, consumed_by_run_id = NULL
|
|
97
|
+
WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
|
|
98
|
+
`).run(isoNow(), projectId, linearIssueId);
|
|
99
|
+
}
|
|
100
|
+
hasPendingIssueSessionEvents(projectId, linearIssueId) {
|
|
101
|
+
const row = this.connection.prepare(`
|
|
102
|
+
SELECT 1
|
|
103
|
+
FROM issue_session_events
|
|
104
|
+
WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
|
|
105
|
+
LIMIT 1
|
|
106
|
+
`).get(projectId, linearIssueId);
|
|
107
|
+
return row !== undefined;
|
|
108
|
+
}
|
|
109
|
+
peekIssueSessionWake(projectId, linearIssueId) {
|
|
110
|
+
const issue = this.getIssue(projectId, linearIssueId);
|
|
111
|
+
if (!issue)
|
|
112
|
+
return undefined;
|
|
113
|
+
const events = this.listIssueSessionEvents(projectId, linearIssueId, { pendingOnly: true });
|
|
114
|
+
const plan = deriveSessionWakePlan(issue, events);
|
|
115
|
+
if (plan?.runType) {
|
|
116
|
+
return {
|
|
117
|
+
eventIds: events.map((event) => event.id),
|
|
118
|
+
runType: plan.runType,
|
|
119
|
+
context: plan.context,
|
|
120
|
+
...(plan.wakeReason ? { wakeReason: plan.wakeReason } : {}),
|
|
121
|
+
resumeThread: plan.resumeThread,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const implicitWake = this.deriveImplicitReactiveWake(issue);
|
|
125
|
+
if (!implicitWake)
|
|
126
|
+
return undefined;
|
|
127
|
+
return {
|
|
128
|
+
eventIds: [],
|
|
129
|
+
runType: implicitWake.runType,
|
|
130
|
+
context: implicitWake.context,
|
|
131
|
+
wakeReason: implicitWake.wakeReason,
|
|
132
|
+
resumeThread: false,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
acquireIssueSessionLease(params) {
|
|
136
|
+
const now = params.now ?? isoNow();
|
|
137
|
+
const result = this.connection.prepare(`
|
|
138
|
+
UPDATE issue_sessions
|
|
139
|
+
SET lease_id = ?, worker_id = ?, leased_until = ?, updated_at = ?
|
|
140
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
141
|
+
AND (leased_until IS NULL OR leased_until <= ? OR lease_id = ?)
|
|
142
|
+
`).run(params.leaseId, params.workerId, params.leasedUntil, now, params.projectId, params.linearIssueId, now, params.leaseId);
|
|
143
|
+
return Number(result.changes ?? 0) > 0;
|
|
144
|
+
}
|
|
145
|
+
forceAcquireIssueSessionLease(params) {
|
|
146
|
+
const now = params.now ?? isoNow();
|
|
147
|
+
const result = this.connection.prepare(`
|
|
148
|
+
UPDATE issue_sessions
|
|
149
|
+
SET lease_id = ?, worker_id = ?, leased_until = ?, updated_at = ?
|
|
150
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
151
|
+
`).run(params.leaseId, params.workerId, params.leasedUntil, now, params.projectId, params.linearIssueId);
|
|
152
|
+
return Number(result.changes ?? 0) > 0;
|
|
153
|
+
}
|
|
154
|
+
renewIssueSessionLease(params) {
|
|
155
|
+
const now = params.now ?? isoNow();
|
|
156
|
+
const result = this.connection.prepare(`
|
|
157
|
+
UPDATE issue_sessions
|
|
158
|
+
SET leased_until = ?, updated_at = ?
|
|
159
|
+
WHERE project_id = ? AND linear_issue_id = ? AND lease_id = ?
|
|
160
|
+
`).run(params.leasedUntil, now, params.projectId, params.linearIssueId, params.leaseId);
|
|
161
|
+
return Number(result.changes ?? 0) > 0;
|
|
162
|
+
}
|
|
163
|
+
releaseIssueSessionLease(projectId, linearIssueId, leaseId) {
|
|
164
|
+
this.connection.prepare(`
|
|
165
|
+
UPDATE issue_sessions
|
|
166
|
+
SET lease_id = NULL, worker_id = NULL, leased_until = NULL, updated_at = ?
|
|
167
|
+
WHERE project_id = ? AND linear_issue_id = ? AND (? IS NULL OR lease_id = ?)
|
|
168
|
+
`).run(isoNow(), projectId, linearIssueId, leaseId ?? null, leaseId ?? null);
|
|
169
|
+
}
|
|
170
|
+
releaseExpiredIssueSessionLeases(now = isoNow()) {
|
|
171
|
+
this.connection.prepare(`
|
|
172
|
+
UPDATE issue_sessions
|
|
173
|
+
SET lease_id = NULL, worker_id = NULL, leased_until = NULL, updated_at = ?
|
|
174
|
+
WHERE leased_until IS NOT NULL AND leased_until <= ?
|
|
175
|
+
`).run(now, now);
|
|
176
|
+
}
|
|
177
|
+
hasActiveIssueSessionLease(projectId, linearIssueId, leaseId, now = isoNow()) {
|
|
178
|
+
const row = this.connection.prepare(`
|
|
179
|
+
SELECT 1
|
|
180
|
+
FROM issue_sessions
|
|
181
|
+
WHERE project_id = ? AND linear_issue_id = ? AND lease_id = ?
|
|
182
|
+
AND leased_until IS NOT NULL
|
|
183
|
+
AND leased_until > ?
|
|
184
|
+
LIMIT 1
|
|
185
|
+
`).get(projectId, linearIssueId, leaseId, now);
|
|
186
|
+
return row !== undefined;
|
|
187
|
+
}
|
|
188
|
+
getActiveIssueSessionLease(projectId, linearIssueId, now = isoNow()) {
|
|
189
|
+
const row = this.connection.prepare(`
|
|
190
|
+
SELECT lease_id
|
|
191
|
+
FROM issue_sessions
|
|
192
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
193
|
+
AND lease_id IS NOT NULL
|
|
194
|
+
AND leased_until IS NOT NULL
|
|
195
|
+
AND leased_until > ?
|
|
196
|
+
LIMIT 1
|
|
197
|
+
`).get(projectId, linearIssueId, now);
|
|
198
|
+
const leaseId = typeof row?.lease_id === "string" ? row.lease_id : undefined;
|
|
199
|
+
if (!leaseId)
|
|
200
|
+
return undefined;
|
|
201
|
+
return { projectId, linearIssueId, leaseId };
|
|
202
|
+
}
|
|
203
|
+
withIssueSessionLease(projectId, linearIssueId, leaseId, fn) {
|
|
204
|
+
return this.transaction(() => {
|
|
205
|
+
if (!this.hasActiveIssueSessionLease(projectId, linearIssueId, leaseId)) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
return fn();
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
upsertIssueWithLease(lease, params) {
|
|
212
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => this.upsertIssue(params));
|
|
213
|
+
}
|
|
214
|
+
upsertIssueRespectingActiveLease(projectId, linearIssueId, params) {
|
|
215
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
216
|
+
if (!lease) {
|
|
217
|
+
return this.upsertIssue(params);
|
|
218
|
+
}
|
|
219
|
+
return this.upsertIssueWithLease(lease, params);
|
|
220
|
+
}
|
|
221
|
+
finishRunWithLease(lease, runId, params) {
|
|
222
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
223
|
+
this.finishRun(runId, params);
|
|
224
|
+
return true;
|
|
225
|
+
}) ?? false;
|
|
226
|
+
}
|
|
227
|
+
finishRunRespectingActiveLease(projectId, linearIssueId, runId, params) {
|
|
228
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
229
|
+
if (!lease) {
|
|
230
|
+
this.finishRun(runId, params);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
return this.finishRunWithLease(lease, runId, params);
|
|
234
|
+
}
|
|
235
|
+
updateRunThreadWithLease(lease, runId, params) {
|
|
236
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
237
|
+
this.updateRunThread(runId, params);
|
|
238
|
+
return true;
|
|
239
|
+
}) ?? false;
|
|
240
|
+
}
|
|
241
|
+
consumeIssueSessionEventsWithLease(lease, eventIds, runId) {
|
|
242
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
243
|
+
this.consumeIssueSessionEvents(lease.projectId, lease.linearIssueId, eventIds, runId);
|
|
244
|
+
return true;
|
|
245
|
+
}) ?? false;
|
|
246
|
+
}
|
|
247
|
+
clearPendingIssueSessionEventsWithLease(lease) {
|
|
248
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
249
|
+
this.clearPendingIssueSessionEvents(lease.projectId, lease.linearIssueId);
|
|
250
|
+
return true;
|
|
251
|
+
}) ?? false;
|
|
252
|
+
}
|
|
253
|
+
clearPendingIssueSessionEventsRespectingActiveLease(projectId, linearIssueId) {
|
|
254
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
255
|
+
if (!lease) {
|
|
256
|
+
this.clearPendingIssueSessionEvents(projectId, linearIssueId);
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
return this.clearPendingIssueSessionEventsWithLease(lease);
|
|
260
|
+
}
|
|
261
|
+
setIssueSessionLastWakeReasonWithLease(lease, lastWakeReason) {
|
|
262
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
263
|
+
this.setIssueSessionLastWakeReason(lease.projectId, lease.linearIssueId, lastWakeReason);
|
|
264
|
+
return true;
|
|
265
|
+
}) ?? false;
|
|
266
|
+
}
|
|
267
|
+
setIssueSessionLastWakeReason(projectId, linearIssueId, lastWakeReason) {
|
|
268
|
+
this.connection.prepare(`
|
|
269
|
+
UPDATE issue_sessions
|
|
270
|
+
SET last_wake_reason = ?, updated_at = ?
|
|
271
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
272
|
+
`).run(lastWakeReason ?? null, isoNow(), projectId, linearIssueId);
|
|
273
|
+
}
|
|
274
|
+
setBranchOwnerWithLease(lease, owner) {
|
|
275
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
276
|
+
this.setBranchOwner(lease.projectId, lease.linearIssueId, owner);
|
|
277
|
+
return true;
|
|
278
|
+
}) ?? false;
|
|
279
|
+
}
|
|
280
|
+
setBranchOwnerRespectingActiveLease(projectId, linearIssueId, owner) {
|
|
281
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
282
|
+
if (!lease) {
|
|
283
|
+
this.setBranchOwner(projectId, linearIssueId, owner);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
return this.setBranchOwnerWithLease(lease, owner);
|
|
287
|
+
}
|
|
288
|
+
releaseIssueSessionLeaseRespectingActiveLease(projectId, linearIssueId) {
|
|
289
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
290
|
+
this.releaseIssueSessionLease(projectId, linearIssueId, lease?.leaseId);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { extractLatestAssistantSummary } from "../issue-session-events.js";
|
|
2
|
+
import { isoNow } from "./shared.js";
|
|
3
|
+
export class RunStore {
|
|
4
|
+
connection;
|
|
5
|
+
mapRunRow;
|
|
6
|
+
getRun;
|
|
7
|
+
getIssue;
|
|
8
|
+
syncIssueSessionFromIssue;
|
|
9
|
+
constructor(connection, mapRunRow, getRun, getIssue, syncIssueSessionFromIssue) {
|
|
10
|
+
this.connection = connection;
|
|
11
|
+
this.mapRunRow = mapRunRow;
|
|
12
|
+
this.getRun = getRun;
|
|
13
|
+
this.getIssue = getIssue;
|
|
14
|
+
this.syncIssueSessionFromIssue = syncIssueSessionFromIssue;
|
|
15
|
+
}
|
|
16
|
+
createRun(params) {
|
|
17
|
+
const now = isoNow();
|
|
18
|
+
const result = this.connection.prepare(`
|
|
19
|
+
INSERT INTO runs (issue_id, project_id, linear_issue_id, run_type, status, source_head_sha, prompt_text, started_at)
|
|
20
|
+
VALUES (?, ?, ?, ?, 'queued', ?, ?, ?)
|
|
21
|
+
`).run(params.issueId, params.projectId, params.linearIssueId, params.runType, params.sourceHeadSha ?? null, params.promptText ?? null, now);
|
|
22
|
+
const run = this.getRun(Number(result.lastInsertRowid));
|
|
23
|
+
const issue = this.getIssue(params.projectId, params.linearIssueId);
|
|
24
|
+
if (issue) {
|
|
25
|
+
this.syncIssueSessionFromIssue(issue, { lastRunType: run.runType });
|
|
26
|
+
}
|
|
27
|
+
return run;
|
|
28
|
+
}
|
|
29
|
+
getRunById(id) {
|
|
30
|
+
const row = this.connection.prepare("SELECT * FROM runs WHERE id = ?").get(id);
|
|
31
|
+
return row ? this.mapRunRow(row) : undefined;
|
|
32
|
+
}
|
|
33
|
+
getRunByThreadId(threadId) {
|
|
34
|
+
const row = this.connection.prepare("SELECT * FROM runs WHERE thread_id = ?").get(threadId);
|
|
35
|
+
return row ? this.mapRunRow(row) : undefined;
|
|
36
|
+
}
|
|
37
|
+
listRunsForIssue(projectId, linearIssueId) {
|
|
38
|
+
const rows = this.connection
|
|
39
|
+
.prepare("SELECT * FROM runs WHERE project_id = ? AND linear_issue_id = ? ORDER BY id")
|
|
40
|
+
.all(projectId, linearIssueId);
|
|
41
|
+
return rows.map(this.mapRunRow);
|
|
42
|
+
}
|
|
43
|
+
getLatestRunForIssue(projectId, linearIssueId) {
|
|
44
|
+
const row = this.connection
|
|
45
|
+
.prepare("SELECT * FROM runs WHERE project_id = ? AND linear_issue_id = ? ORDER BY id DESC LIMIT 1")
|
|
46
|
+
.get(projectId, linearIssueId);
|
|
47
|
+
return row ? this.mapRunRow(row) : undefined;
|
|
48
|
+
}
|
|
49
|
+
listActiveRuns() {
|
|
50
|
+
const rows = this.connection
|
|
51
|
+
.prepare("SELECT * FROM runs WHERE status IN ('queued', 'running')")
|
|
52
|
+
.all();
|
|
53
|
+
return rows.map(this.mapRunRow);
|
|
54
|
+
}
|
|
55
|
+
listRunningRuns() {
|
|
56
|
+
const rows = this.connection
|
|
57
|
+
.prepare("SELECT * FROM runs WHERE status IN ('running', 'queued')")
|
|
58
|
+
.all();
|
|
59
|
+
return rows.map(this.mapRunRow);
|
|
60
|
+
}
|
|
61
|
+
updateRunThread(runId, params) {
|
|
62
|
+
this.connection.prepare(`
|
|
63
|
+
UPDATE runs SET
|
|
64
|
+
thread_id = ?,
|
|
65
|
+
parent_thread_id = COALESCE(?, parent_thread_id),
|
|
66
|
+
turn_id = COALESCE(?, turn_id),
|
|
67
|
+
status = 'running'
|
|
68
|
+
WHERE id = ?
|
|
69
|
+
AND ended_at IS NULL
|
|
70
|
+
AND status IN ('queued', 'running')
|
|
71
|
+
`).run(params.threadId, params.parentThreadId ?? null, params.turnId ?? null, runId);
|
|
72
|
+
const run = this.getRun(runId);
|
|
73
|
+
if (!run)
|
|
74
|
+
return;
|
|
75
|
+
const issue = this.getIssue(run.projectId, run.linearIssueId);
|
|
76
|
+
if (issue) {
|
|
77
|
+
this.syncIssueSessionFromIssue(issue);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
updateRunTurnId(runId, turnId) {
|
|
81
|
+
this.connection.prepare("UPDATE runs SET turn_id = ? WHERE id = ?").run(turnId, runId);
|
|
82
|
+
}
|
|
83
|
+
finishRun(runId, params) {
|
|
84
|
+
const now = isoNow();
|
|
85
|
+
this.connection.prepare(`
|
|
86
|
+
UPDATE runs SET
|
|
87
|
+
status = ?,
|
|
88
|
+
thread_id = COALESCE(?, thread_id),
|
|
89
|
+
turn_id = COALESCE(?, turn_id),
|
|
90
|
+
failure_reason = COALESCE(?, failure_reason),
|
|
91
|
+
summary_json = COALESCE(?, summary_json),
|
|
92
|
+
report_json = COALESCE(?, report_json),
|
|
93
|
+
ended_at = ?
|
|
94
|
+
WHERE id = ?
|
|
95
|
+
`).run(params.status, params.threadId ?? null, params.turnId ?? null, params.failureReason ?? null, params.summaryJson ?? null, params.reportJson ?? null, now, runId);
|
|
96
|
+
const run = this.getRun(runId);
|
|
97
|
+
if (!run)
|
|
98
|
+
return;
|
|
99
|
+
const issue = this.getIssue(run.projectId, run.linearIssueId);
|
|
100
|
+
if (issue) {
|
|
101
|
+
this.syncIssueSessionFromIssue(issue, {
|
|
102
|
+
summaryText: extractLatestAssistantSummary(this.getRun(runId) ?? run),
|
|
103
|
+
lastRunType: run.runType,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
saveThreadEvent(params) {
|
|
108
|
+
this.connection.prepare(`
|
|
109
|
+
INSERT INTO run_thread_events (run_id, thread_id, turn_id, method, event_json, created_at)
|
|
110
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
111
|
+
`).run(params.runId, params.threadId, params.turnId ?? null, params.method, params.eventJson, isoNow());
|
|
112
|
+
}
|
|
113
|
+
listThreadEvents(runId) {
|
|
114
|
+
const rows = this.connection
|
|
115
|
+
.prepare("SELECT * FROM run_thread_events WHERE run_id = ? ORDER BY id")
|
|
116
|
+
.all(runId);
|
|
117
|
+
return rows.map((row) => ({
|
|
118
|
+
id: Number(row.id),
|
|
119
|
+
runId: Number(row.run_id),
|
|
120
|
+
threadId: String(row.thread_id),
|
|
121
|
+
...(row.turn_id !== null ? { turnId: String(row.turn_id) } : {}),
|
|
122
|
+
method: String(row.method),
|
|
123
|
+
eventJson: String(row.event_json),
|
|
124
|
+
createdAt: String(row.created_at),
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export class WebhookEventStore {
|
|
2
|
+
connection;
|
|
3
|
+
constructor(connection) {
|
|
4
|
+
this.connection = connection;
|
|
5
|
+
}
|
|
6
|
+
insertWebhookEvent(webhookId, receivedAt) {
|
|
7
|
+
const existing = this.connection
|
|
8
|
+
.prepare("SELECT id FROM webhook_events WHERE webhook_id = ?")
|
|
9
|
+
.get(webhookId);
|
|
10
|
+
if (existing) {
|
|
11
|
+
return { id: existing.id, duplicate: true };
|
|
12
|
+
}
|
|
13
|
+
const result = this.connection
|
|
14
|
+
.prepare("INSERT INTO webhook_events (webhook_id, received_at, processing_status) VALUES (?, ?, 'processed')")
|
|
15
|
+
.run(webhookId, receivedAt);
|
|
16
|
+
return { id: Number(result.lastInsertRowid), duplicate: false };
|
|
17
|
+
}
|
|
18
|
+
insertFullWebhookEvent(params) {
|
|
19
|
+
const existing = this.connection
|
|
20
|
+
.prepare("SELECT id FROM webhook_events WHERE webhook_id = ?")
|
|
21
|
+
.get(params.webhookId);
|
|
22
|
+
if (existing) {
|
|
23
|
+
return { id: existing.id, dedupeStatus: "duplicate" };
|
|
24
|
+
}
|
|
25
|
+
const result = this.connection
|
|
26
|
+
.prepare("INSERT INTO webhook_events (webhook_id, received_at, payload_json) VALUES (?, ?, ?)")
|
|
27
|
+
.run(params.webhookId, params.receivedAt, params.payloadJson);
|
|
28
|
+
return { id: Number(result.lastInsertRowid), dedupeStatus: "accepted" };
|
|
29
|
+
}
|
|
30
|
+
getWebhookPayload(id) {
|
|
31
|
+
const row = this.connection.prepare("SELECT webhook_id, payload_json FROM webhook_events WHERE id = ?").get(id);
|
|
32
|
+
if (!row || !row.payload_json)
|
|
33
|
+
return undefined;
|
|
34
|
+
return { webhookId: String(row.webhook_id), payloadJson: String(row.payload_json) };
|
|
35
|
+
}
|
|
36
|
+
isWebhookDuplicate(webhookId) {
|
|
37
|
+
return this.connection.prepare("SELECT 1 FROM webhook_events WHERE webhook_id = ?").get(webhookId) !== undefined;
|
|
38
|
+
}
|
|
39
|
+
markWebhookProcessed(id, status) {
|
|
40
|
+
this.connection.prepare("UPDATE webhook_events SET processing_status = ? WHERE id = ?").run(status, id);
|
|
41
|
+
}
|
|
42
|
+
assignWebhookProject(id, projectId) {
|
|
43
|
+
this.connection.prepare("UPDATE webhook_events SET project_id = ? WHERE id = ?").run(projectId, id);
|
|
44
|
+
}
|
|
45
|
+
findLatestAgentSessionIdForIssue(linearIssueId) {
|
|
46
|
+
const row = this.connection.prepare(`
|
|
47
|
+
SELECT COALESCE(
|
|
48
|
+
json_extract(payload_json, '$.agentSession.id'),
|
|
49
|
+
json_extract(payload_json, '$.data.agentSession.id'),
|
|
50
|
+
json_extract(payload_json, '$.agentSessionId'),
|
|
51
|
+
json_extract(payload_json, '$.data.agentSessionId')
|
|
52
|
+
) AS agent_session_id
|
|
53
|
+
FROM webhook_events
|
|
54
|
+
WHERE COALESCE(
|
|
55
|
+
json_extract(payload_json, '$.agentSession.issueId'),
|
|
56
|
+
json_extract(payload_json, '$.data.agentSession.issueId'),
|
|
57
|
+
json_extract(payload_json, '$.agentSession.issue.id'),
|
|
58
|
+
json_extract(payload_json, '$.data.agentSession.issue.id')
|
|
59
|
+
) = ?
|
|
60
|
+
AND COALESCE(
|
|
61
|
+
json_extract(payload_json, '$.agentSession.id'),
|
|
62
|
+
json_extract(payload_json, '$.data.agentSession.id'),
|
|
63
|
+
json_extract(payload_json, '$.agentSessionId'),
|
|
64
|
+
json_extract(payload_json, '$.data.agentSessionId')
|
|
65
|
+
) IS NOT NULL
|
|
66
|
+
ORDER BY id DESC
|
|
67
|
+
LIMIT 1
|
|
68
|
+
`).get(linearIssueId);
|
|
69
|
+
return row?.agent_session_id != null ? String(row.agent_session_id) : undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|