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/db.js
CHANGED
|
@@ -1,9 +1,90 @@
|
|
|
1
|
+
import { isIssueSessionReadyForExecution, deriveIssueSessionState, deriveIssueSessionReactiveIntent, deriveIssueSessionWakeReason, } from "./issue-session.js";
|
|
2
|
+
import { deriveSessionWakePlan, extractLatestAssistantSummary, } from "./issue-session-events.js";
|
|
1
3
|
import { parseGitHubFailureContext } from "./github-failure-context.js";
|
|
4
|
+
import { deriveIssueStatusNote } from "./status-note.js";
|
|
2
5
|
import { LinearInstallationStore } from "./db/linear-installation-store.js";
|
|
3
6
|
import { OperatorFeedStore } from "./db/operator-feed-store.js";
|
|
4
7
|
import { RepositoryLinkStore } from "./db/repository-link-store.js";
|
|
5
8
|
import { runPatchRelayMigrations } from "./db/migrations.js";
|
|
6
9
|
import { SqliteConnection, isoNow } from "./db/shared.js";
|
|
10
|
+
import { derivePatchRelayWaitingReason } from "./waiting-reason.js";
|
|
11
|
+
function parseObjectJson(raw) {
|
|
12
|
+
if (!raw)
|
|
13
|
+
return undefined;
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
17
|
+
? parsed
|
|
18
|
+
: undefined;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function hasUnattemptedFailureSignature(issue, fallbackHeadSha) {
|
|
25
|
+
const signature = issue.lastGitHubFailureSignature;
|
|
26
|
+
if (!signature)
|
|
27
|
+
return false;
|
|
28
|
+
const headSha = issue.lastGitHubFailureHeadSha ?? fallbackHeadSha;
|
|
29
|
+
return issue.lastAttemptedFailureSignature !== signature
|
|
30
|
+
|| (headSha !== undefined && issue.lastAttemptedFailureHeadSha !== headSha);
|
|
31
|
+
}
|
|
32
|
+
function deriveImplicitReactiveWake(issue) {
|
|
33
|
+
const reactiveIntent = deriveIssueSessionReactiveIntent({
|
|
34
|
+
activeRunId: issue.activeRunId,
|
|
35
|
+
prNumber: issue.prNumber,
|
|
36
|
+
prState: issue.prState,
|
|
37
|
+
prReviewState: issue.prReviewState,
|
|
38
|
+
prCheckStatus: issue.prCheckStatus,
|
|
39
|
+
latestFailureSource: issue.lastGitHubFailureSource,
|
|
40
|
+
});
|
|
41
|
+
if (!reactiveIntent)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (reactiveIntent.runType === "ci_repair") {
|
|
44
|
+
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
|
|
45
|
+
const snapshot = parseObjectJson(issue.lastGitHubCiSnapshotJson);
|
|
46
|
+
const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
|
|
47
|
+
? failureContext.failureHeadSha
|
|
48
|
+
: issue.lastGitHubFailureHeadSha ?? issue.prHeadSha;
|
|
49
|
+
const failureSignature = issue.lastGitHubFailureSignature
|
|
50
|
+
?? (fallbackHeadSha ? `implicit_branch_ci::${fallbackHeadSha}` : undefined);
|
|
51
|
+
if (!failureSignature || issue.prState !== "open")
|
|
52
|
+
return undefined;
|
|
53
|
+
if (issue.lastAttemptedFailureSignature === failureSignature
|
|
54
|
+
&& (fallbackHeadSha === undefined || issue.lastAttemptedFailureHeadSha === fallbackHeadSha)) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
runType: reactiveIntent.runType,
|
|
59
|
+
wakeReason: reactiveIntent.wakeReason,
|
|
60
|
+
context: {
|
|
61
|
+
...failureContext,
|
|
62
|
+
failureSignature,
|
|
63
|
+
...(fallbackHeadSha ? { failureHeadSha: fallbackHeadSha } : {}),
|
|
64
|
+
...(issue.lastGitHubFailureCheckName ? { checkName: issue.lastGitHubFailureCheckName } : {}),
|
|
65
|
+
...(snapshot ? { ciSnapshot: snapshot } : {}),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (reactiveIntent.runType === "queue_repair") {
|
|
70
|
+
const failureContext = parseObjectJson(issue.lastGitHubFailureContextJson) ?? {};
|
|
71
|
+
const incidentContext = parseObjectJson(issue.lastQueueIncidentJson) ?? {};
|
|
72
|
+
const fallbackHeadSha = typeof failureContext.failureHeadSha === "string"
|
|
73
|
+
? failureContext.failureHeadSha
|
|
74
|
+
: undefined;
|
|
75
|
+
if (!hasUnattemptedFailureSignature(issue, fallbackHeadSha))
|
|
76
|
+
return undefined;
|
|
77
|
+
return {
|
|
78
|
+
runType: reactiveIntent.runType,
|
|
79
|
+
wakeReason: reactiveIntent.wakeReason,
|
|
80
|
+
context: {
|
|
81
|
+
...incidentContext,
|
|
82
|
+
...failureContext,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
7
88
|
export class PatchRelayDatabase {
|
|
8
89
|
connection;
|
|
9
90
|
linearInstallations;
|
|
@@ -137,6 +218,10 @@ export class PatchRelayDatabase {
|
|
|
137
218
|
sets.push("active_run_id = @activeRunId");
|
|
138
219
|
values.activeRunId = params.activeRunId;
|
|
139
220
|
}
|
|
221
|
+
if (params.statusCommentId !== undefined) {
|
|
222
|
+
sets.push("status_comment_id = @statusCommentId");
|
|
223
|
+
values.statusCommentId = params.statusCommentId;
|
|
224
|
+
}
|
|
140
225
|
if (params.agentSessionId !== undefined) {
|
|
141
226
|
sets.push("agent_session_id = @agentSessionId");
|
|
142
227
|
values.agentSessionId = params.agentSessionId;
|
|
@@ -153,6 +238,14 @@ export class PatchRelayDatabase {
|
|
|
153
238
|
sets.push("pr_state = @prState");
|
|
154
239
|
values.prState = params.prState;
|
|
155
240
|
}
|
|
241
|
+
if (params.prHeadSha !== undefined) {
|
|
242
|
+
sets.push("pr_head_sha = @prHeadSha");
|
|
243
|
+
values.prHeadSha = params.prHeadSha;
|
|
244
|
+
}
|
|
245
|
+
if (params.prAuthorLogin !== undefined) {
|
|
246
|
+
sets.push("pr_author_login = @prAuthorLogin");
|
|
247
|
+
values.prAuthorLogin = params.prAuthorLogin;
|
|
248
|
+
}
|
|
156
249
|
if (params.prReviewState !== undefined) {
|
|
157
250
|
sets.push("pr_review_state = @prReviewState");
|
|
158
251
|
values.prReviewState = params.prReviewState;
|
|
@@ -245,10 +338,6 @@ export class PatchRelayDatabase {
|
|
|
245
338
|
sets.push("last_zombie_recovery_at = @lastZombieRecoveryAt");
|
|
246
339
|
values.lastZombieRecoveryAt = params.lastZombieRecoveryAt;
|
|
247
340
|
}
|
|
248
|
-
if (params.queueLabelApplied !== undefined) {
|
|
249
|
-
sets.push("queue_label_applied = @queueLabelApplied");
|
|
250
|
-
values.queueLabelApplied = params.queueLabelApplied ? 1 : 0;
|
|
251
|
-
}
|
|
252
341
|
this.connection.prepare(`UPDATE issues SET ${sets.join(", ")} WHERE project_id = @projectId AND linear_issue_id = @linearIssueId`).run(values);
|
|
253
342
|
}
|
|
254
343
|
else {
|
|
@@ -257,9 +346,9 @@ export class PatchRelayDatabase {
|
|
|
257
346
|
project_id, linear_issue_id, issue_key, title, description, url,
|
|
258
347
|
priority, estimate,
|
|
259
348
|
current_linear_state, current_linear_state_type, factory_state, pending_run_type, pending_run_context_json,
|
|
260
|
-
branch_name, worktree_path, thread_id, active_run_id,
|
|
349
|
+
branch_name, worktree_path, thread_id, active_run_id, status_comment_id,
|
|
261
350
|
agent_session_id,
|
|
262
|
-
pr_number, pr_url, pr_state, pr_review_state, pr_check_status,
|
|
351
|
+
pr_number, pr_url, pr_state, pr_head_sha, pr_author_login, pr_review_state, pr_check_status,
|
|
263
352
|
last_github_failure_source, last_github_failure_head_sha, last_github_failure_signature, last_github_failure_check_name, last_github_failure_check_url, last_github_failure_context_json, last_github_failure_at,
|
|
264
353
|
last_github_ci_snapshot_head_sha, last_github_ci_snapshot_gate_check_name, last_github_ci_snapshot_gate_check_status, last_github_ci_snapshot_json, last_github_ci_snapshot_settled_at,
|
|
265
354
|
last_queue_signal_at, last_queue_incident_json,
|
|
@@ -269,9 +358,9 @@ export class PatchRelayDatabase {
|
|
|
269
358
|
@projectId, @linearIssueId, @issueKey, @title, @description, @url,
|
|
270
359
|
@priority, @estimate,
|
|
271
360
|
@currentLinearState, @currentLinearStateType, @factoryState, @pendingRunType, @pendingRunContextJson,
|
|
272
|
-
@branchName, @worktreePath, @threadId, @activeRunId,
|
|
361
|
+
@branchName, @worktreePath, @threadId, @activeRunId, @statusCommentId,
|
|
273
362
|
@agentSessionId,
|
|
274
|
-
@prNumber, @prUrl, @prState, @prReviewState, @prCheckStatus,
|
|
363
|
+
@prNumber, @prUrl, @prState, @prHeadSha, @prAuthorLogin, @prReviewState, @prCheckStatus,
|
|
275
364
|
@lastGitHubFailureSource, @lastGitHubFailureHeadSha, @lastGitHubFailureSignature, @lastGitHubFailureCheckName, @lastGitHubFailureCheckUrl, @lastGitHubFailureContextJson, @lastGitHubFailureAt,
|
|
276
365
|
@lastGitHubCiSnapshotHeadSha, @lastGitHubCiSnapshotGateCheckName, @lastGitHubCiSnapshotGateCheckStatus, @lastGitHubCiSnapshotJson, @lastGitHubCiSnapshotSettledAt,
|
|
277
366
|
@lastQueueSignalAt, @lastQueueIncidentJson,
|
|
@@ -296,10 +385,13 @@ export class PatchRelayDatabase {
|
|
|
296
385
|
worktreePath: params.worktreePath ?? null,
|
|
297
386
|
threadId: params.threadId ?? null,
|
|
298
387
|
activeRunId: params.activeRunId ?? null,
|
|
388
|
+
statusCommentId: params.statusCommentId ?? null,
|
|
299
389
|
agentSessionId: params.agentSessionId ?? null,
|
|
300
390
|
prNumber: params.prNumber ?? null,
|
|
301
391
|
prUrl: params.prUrl ?? null,
|
|
302
392
|
prState: params.prState ?? null,
|
|
393
|
+
prHeadSha: params.prHeadSha ?? null,
|
|
394
|
+
prAuthorLogin: params.prAuthorLogin ?? null,
|
|
303
395
|
prReviewState: params.prReviewState ?? null,
|
|
304
396
|
prCheckStatus: params.prCheckStatus ?? null,
|
|
305
397
|
lastGitHubFailureSource: params.lastGitHubFailureSource ?? null,
|
|
@@ -321,7 +413,9 @@ export class PatchRelayDatabase {
|
|
|
321
413
|
now,
|
|
322
414
|
});
|
|
323
415
|
}
|
|
324
|
-
|
|
416
|
+
const updated = this.getIssue(params.projectId, params.linearIssueId);
|
|
417
|
+
this.syncIssueSessionFromIssue(updated);
|
|
418
|
+
return updated;
|
|
325
419
|
}
|
|
326
420
|
getIssue(projectId, linearIssueId) {
|
|
327
421
|
const row = this.connection
|
|
@@ -345,6 +439,254 @@ export class PatchRelayDatabase {
|
|
|
345
439
|
const row = this.connection.prepare("SELECT * FROM issues WHERE pr_number = ?").get(prNumber);
|
|
346
440
|
return row ? mapIssueRow(row) : undefined;
|
|
347
441
|
}
|
|
442
|
+
getIssueSession(projectId, linearIssueId) {
|
|
443
|
+
const row = this.connection
|
|
444
|
+
.prepare("SELECT * FROM issue_sessions WHERE project_id = ? AND linear_issue_id = ?")
|
|
445
|
+
.get(projectId, linearIssueId);
|
|
446
|
+
return row ? mapIssueSessionRow(row) : undefined;
|
|
447
|
+
}
|
|
448
|
+
getIssueSessionByKey(issueKey) {
|
|
449
|
+
const row = this.connection.prepare("SELECT * FROM issue_sessions WHERE issue_key = ?").get(issueKey);
|
|
450
|
+
return row ? mapIssueSessionRow(row) : undefined;
|
|
451
|
+
}
|
|
452
|
+
appendIssueSessionEvent(params) {
|
|
453
|
+
if (params.dedupeKey) {
|
|
454
|
+
const existing = this.connection.prepare(`
|
|
455
|
+
SELECT * FROM issue_session_events
|
|
456
|
+
WHERE project_id = ? AND linear_issue_id = ? AND dedupe_key = ? AND processed_at IS NULL
|
|
457
|
+
ORDER BY id DESC LIMIT 1
|
|
458
|
+
`).get(params.projectId, params.linearIssueId, params.dedupeKey);
|
|
459
|
+
if (existing)
|
|
460
|
+
return mapIssueSessionEventRow(existing);
|
|
461
|
+
}
|
|
462
|
+
const now = isoNow();
|
|
463
|
+
const result = this.connection.prepare(`
|
|
464
|
+
INSERT INTO issue_session_events (
|
|
465
|
+
project_id, linear_issue_id, event_type, event_json, dedupe_key, created_at
|
|
466
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
467
|
+
`).run(params.projectId, params.linearIssueId, params.eventType, params.eventJson ?? null, params.dedupeKey ?? null, now);
|
|
468
|
+
return this.getIssueSessionEvent(Number(result.lastInsertRowid));
|
|
469
|
+
}
|
|
470
|
+
appendIssueSessionEventWithLease(lease, params) {
|
|
471
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => this.appendIssueSessionEvent(params));
|
|
472
|
+
}
|
|
473
|
+
appendIssueSessionEventRespectingActiveLease(projectId, linearIssueId, params) {
|
|
474
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
475
|
+
if (!lease) {
|
|
476
|
+
return this.appendIssueSessionEvent(params);
|
|
477
|
+
}
|
|
478
|
+
return this.appendIssueSessionEventWithLease(lease, params);
|
|
479
|
+
}
|
|
480
|
+
getIssueSessionEvent(id) {
|
|
481
|
+
const row = this.connection.prepare("SELECT * FROM issue_session_events WHERE id = ?").get(id);
|
|
482
|
+
return row ? mapIssueSessionEventRow(row) : undefined;
|
|
483
|
+
}
|
|
484
|
+
listIssueSessionEvents(projectId, linearIssueId, options) {
|
|
485
|
+
const conditions = ["project_id = ?", "linear_issue_id = ?"];
|
|
486
|
+
const values = [projectId, linearIssueId];
|
|
487
|
+
if (options?.pendingOnly) {
|
|
488
|
+
conditions.push("processed_at IS NULL");
|
|
489
|
+
}
|
|
490
|
+
let query = `SELECT * FROM issue_session_events WHERE ${conditions.join(" AND ")} ORDER BY id`;
|
|
491
|
+
if (options?.limit !== undefined) {
|
|
492
|
+
query += " LIMIT ?";
|
|
493
|
+
values.push(options.limit);
|
|
494
|
+
}
|
|
495
|
+
const rows = this.connection.prepare(query).all(...values);
|
|
496
|
+
return rows.map(mapIssueSessionEventRow);
|
|
497
|
+
}
|
|
498
|
+
consumeIssueSessionEvents(projectId, linearIssueId, eventIds, runId) {
|
|
499
|
+
if (eventIds.length === 0)
|
|
500
|
+
return;
|
|
501
|
+
const now = isoNow();
|
|
502
|
+
const placeholders = eventIds.map(() => "?").join(", ");
|
|
503
|
+
this.connection.prepare(`
|
|
504
|
+
UPDATE issue_session_events
|
|
505
|
+
SET processed_at = ?, consumed_by_run_id = ?
|
|
506
|
+
WHERE project_id = ? AND linear_issue_id = ? AND id IN (${placeholders}) AND processed_at IS NULL
|
|
507
|
+
`).run(now, runId, projectId, linearIssueId, ...eventIds);
|
|
508
|
+
}
|
|
509
|
+
clearPendingIssueSessionEvents(projectId, linearIssueId) {
|
|
510
|
+
this.connection.prepare(`
|
|
511
|
+
UPDATE issue_session_events
|
|
512
|
+
SET processed_at = ?, consumed_by_run_id = NULL
|
|
513
|
+
WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
|
|
514
|
+
`).run(isoNow(), projectId, linearIssueId);
|
|
515
|
+
}
|
|
516
|
+
hasPendingIssueSessionEvents(projectId, linearIssueId) {
|
|
517
|
+
const row = this.connection.prepare(`
|
|
518
|
+
SELECT 1
|
|
519
|
+
FROM issue_session_events
|
|
520
|
+
WHERE project_id = ? AND linear_issue_id = ? AND processed_at IS NULL
|
|
521
|
+
LIMIT 1
|
|
522
|
+
`).get(projectId, linearIssueId);
|
|
523
|
+
return row !== undefined;
|
|
524
|
+
}
|
|
525
|
+
peekIssueSessionWake(projectId, linearIssueId) {
|
|
526
|
+
const issue = this.getIssue(projectId, linearIssueId);
|
|
527
|
+
if (!issue)
|
|
528
|
+
return undefined;
|
|
529
|
+
const events = this.listIssueSessionEvents(projectId, linearIssueId, { pendingOnly: true });
|
|
530
|
+
const plan = deriveSessionWakePlan(issue, events);
|
|
531
|
+
if (plan?.runType) {
|
|
532
|
+
return {
|
|
533
|
+
eventIds: events.map((event) => event.id),
|
|
534
|
+
runType: plan.runType,
|
|
535
|
+
context: plan.context,
|
|
536
|
+
...(plan.wakeReason ? { wakeReason: plan.wakeReason } : {}),
|
|
537
|
+
resumeThread: plan.resumeThread,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
const implicitWake = deriveImplicitReactiveWake(issue);
|
|
541
|
+
if (!implicitWake)
|
|
542
|
+
return undefined;
|
|
543
|
+
return {
|
|
544
|
+
eventIds: [],
|
|
545
|
+
runType: implicitWake.runType,
|
|
546
|
+
context: implicitWake.context,
|
|
547
|
+
wakeReason: implicitWake.wakeReason,
|
|
548
|
+
resumeThread: false,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
acquireIssueSessionLease(params) {
|
|
552
|
+
const now = params.now ?? isoNow();
|
|
553
|
+
const result = this.connection.prepare(`
|
|
554
|
+
UPDATE issue_sessions
|
|
555
|
+
SET lease_id = ?, worker_id = ?, leased_until = ?, updated_at = ?
|
|
556
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
557
|
+
AND (leased_until IS NULL OR leased_until <= ? OR lease_id = ?)
|
|
558
|
+
`).run(params.leaseId, params.workerId, params.leasedUntil, now, params.projectId, params.linearIssueId, now, params.leaseId);
|
|
559
|
+
return Number(result.changes ?? 0) > 0;
|
|
560
|
+
}
|
|
561
|
+
forceAcquireIssueSessionLease(params) {
|
|
562
|
+
const now = params.now ?? isoNow();
|
|
563
|
+
const result = this.connection.prepare(`
|
|
564
|
+
UPDATE issue_sessions
|
|
565
|
+
SET lease_id = ?, worker_id = ?, leased_until = ?, updated_at = ?
|
|
566
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
567
|
+
`).run(params.leaseId, params.workerId, params.leasedUntil, now, params.projectId, params.linearIssueId);
|
|
568
|
+
return Number(result.changes ?? 0) > 0;
|
|
569
|
+
}
|
|
570
|
+
renewIssueSessionLease(params) {
|
|
571
|
+
const now = params.now ?? isoNow();
|
|
572
|
+
const result = this.connection.prepare(`
|
|
573
|
+
UPDATE issue_sessions
|
|
574
|
+
SET leased_until = ?, updated_at = ?
|
|
575
|
+
WHERE project_id = ? AND linear_issue_id = ? AND lease_id = ?
|
|
576
|
+
`).run(params.leasedUntil, now, params.projectId, params.linearIssueId, params.leaseId);
|
|
577
|
+
return Number(result.changes ?? 0) > 0;
|
|
578
|
+
}
|
|
579
|
+
releaseIssueSessionLease(projectId, linearIssueId, leaseId) {
|
|
580
|
+
this.connection.prepare(`
|
|
581
|
+
UPDATE issue_sessions
|
|
582
|
+
SET lease_id = NULL, worker_id = NULL, leased_until = NULL, updated_at = ?
|
|
583
|
+
WHERE project_id = ? AND linear_issue_id = ? AND (? IS NULL OR lease_id = ?)
|
|
584
|
+
`).run(isoNow(), projectId, linearIssueId, leaseId ?? null, leaseId ?? null);
|
|
585
|
+
}
|
|
586
|
+
releaseExpiredIssueSessionLeases(now = isoNow()) {
|
|
587
|
+
this.connection.prepare(`
|
|
588
|
+
UPDATE issue_sessions
|
|
589
|
+
SET lease_id = NULL, worker_id = NULL, leased_until = NULL, updated_at = ?
|
|
590
|
+
WHERE leased_until IS NOT NULL AND leased_until <= ?
|
|
591
|
+
`).run(now, now);
|
|
592
|
+
}
|
|
593
|
+
hasActiveIssueSessionLease(projectId, linearIssueId, leaseId, now = isoNow()) {
|
|
594
|
+
const row = this.connection.prepare(`
|
|
595
|
+
SELECT 1
|
|
596
|
+
FROM issue_sessions
|
|
597
|
+
WHERE project_id = ? AND linear_issue_id = ? AND lease_id = ?
|
|
598
|
+
AND leased_until IS NOT NULL
|
|
599
|
+
AND leased_until > ?
|
|
600
|
+
LIMIT 1
|
|
601
|
+
`).get(projectId, linearIssueId, leaseId, now);
|
|
602
|
+
return row !== undefined;
|
|
603
|
+
}
|
|
604
|
+
getActiveIssueSessionLease(projectId, linearIssueId, now = isoNow()) {
|
|
605
|
+
const row = this.connection.prepare(`
|
|
606
|
+
SELECT lease_id
|
|
607
|
+
FROM issue_sessions
|
|
608
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
609
|
+
AND lease_id IS NOT NULL
|
|
610
|
+
AND leased_until IS NOT NULL
|
|
611
|
+
AND leased_until > ?
|
|
612
|
+
LIMIT 1
|
|
613
|
+
`).get(projectId, linearIssueId, now);
|
|
614
|
+
const leaseId = typeof row?.lease_id === "string" ? row.lease_id : undefined;
|
|
615
|
+
if (!leaseId)
|
|
616
|
+
return undefined;
|
|
617
|
+
return { projectId, linearIssueId, leaseId };
|
|
618
|
+
}
|
|
619
|
+
withIssueSessionLease(projectId, linearIssueId, leaseId, fn) {
|
|
620
|
+
return this.transaction(() => {
|
|
621
|
+
if (!this.hasActiveIssueSessionLease(projectId, linearIssueId, leaseId)) {
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
624
|
+
return fn();
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
upsertIssueWithLease(lease, params) {
|
|
628
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => this.upsertIssue(params));
|
|
629
|
+
}
|
|
630
|
+
upsertIssueRespectingActiveLease(projectId, linearIssueId, params) {
|
|
631
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
632
|
+
if (!lease) {
|
|
633
|
+
return this.upsertIssue(params);
|
|
634
|
+
}
|
|
635
|
+
return this.upsertIssueWithLease(lease, params);
|
|
636
|
+
}
|
|
637
|
+
finishRunWithLease(lease, runId, params) {
|
|
638
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
639
|
+
this.finishRun(runId, params);
|
|
640
|
+
return true;
|
|
641
|
+
}) ?? false;
|
|
642
|
+
}
|
|
643
|
+
finishRunRespectingActiveLease(projectId, linearIssueId, runId, params) {
|
|
644
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
645
|
+
if (!lease) {
|
|
646
|
+
this.finishRun(runId, params);
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
return this.finishRunWithLease(lease, runId, params);
|
|
650
|
+
}
|
|
651
|
+
updateRunThreadWithLease(lease, runId, params) {
|
|
652
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
653
|
+
this.updateRunThread(runId, params);
|
|
654
|
+
return true;
|
|
655
|
+
}) ?? false;
|
|
656
|
+
}
|
|
657
|
+
consumeIssueSessionEventsWithLease(lease, eventIds, runId) {
|
|
658
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
659
|
+
this.consumeIssueSessionEvents(lease.projectId, lease.linearIssueId, eventIds, runId);
|
|
660
|
+
return true;
|
|
661
|
+
}) ?? false;
|
|
662
|
+
}
|
|
663
|
+
clearPendingIssueSessionEventsWithLease(lease) {
|
|
664
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
665
|
+
this.clearPendingIssueSessionEvents(lease.projectId, lease.linearIssueId);
|
|
666
|
+
return true;
|
|
667
|
+
}) ?? false;
|
|
668
|
+
}
|
|
669
|
+
clearPendingIssueSessionEventsRespectingActiveLease(projectId, linearIssueId) {
|
|
670
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
671
|
+
if (!lease) {
|
|
672
|
+
this.clearPendingIssueSessionEvents(projectId, linearIssueId);
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
return this.clearPendingIssueSessionEventsWithLease(lease);
|
|
676
|
+
}
|
|
677
|
+
setIssueSessionLastWakeReasonWithLease(lease, lastWakeReason) {
|
|
678
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
679
|
+
this.setIssueSessionLastWakeReason(lease.projectId, lease.linearIssueId, lastWakeReason);
|
|
680
|
+
return true;
|
|
681
|
+
}) ?? false;
|
|
682
|
+
}
|
|
683
|
+
setIssueSessionLastWakeReason(projectId, linearIssueId, lastWakeReason) {
|
|
684
|
+
this.connection.prepare(`
|
|
685
|
+
UPDATE issue_sessions
|
|
686
|
+
SET last_wake_reason = ?, updated_at = ?
|
|
687
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
688
|
+
`).run(lastWakeReason ?? null, isoNow(), projectId, linearIssueId);
|
|
689
|
+
}
|
|
348
690
|
setBranchOwner(projectId, linearIssueId, owner) {
|
|
349
691
|
this.connection.prepare(`
|
|
350
692
|
UPDATE issues
|
|
@@ -352,6 +694,24 @@ export class PatchRelayDatabase {
|
|
|
352
694
|
WHERE project_id = ? AND linear_issue_id = ?
|
|
353
695
|
`).run(owner, isoNow(), isoNow(), projectId, linearIssueId);
|
|
354
696
|
}
|
|
697
|
+
setBranchOwnerWithLease(lease, owner) {
|
|
698
|
+
return this.withIssueSessionLease(lease.projectId, lease.linearIssueId, lease.leaseId, () => {
|
|
699
|
+
this.setBranchOwner(lease.projectId, lease.linearIssueId, owner);
|
|
700
|
+
return true;
|
|
701
|
+
}) ?? false;
|
|
702
|
+
}
|
|
703
|
+
setBranchOwnerRespectingActiveLease(projectId, linearIssueId, owner) {
|
|
704
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
705
|
+
if (!lease) {
|
|
706
|
+
this.setBranchOwner(projectId, linearIssueId, owner);
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
return this.setBranchOwnerWithLease(lease, owner);
|
|
710
|
+
}
|
|
711
|
+
releaseIssueSessionLeaseRespectingActiveLease(projectId, linearIssueId) {
|
|
712
|
+
const lease = this.getActiveIssueSessionLease(projectId, linearIssueId);
|
|
713
|
+
this.releaseIssueSessionLease(projectId, linearIssueId, lease?.leaseId);
|
|
714
|
+
}
|
|
355
715
|
replaceIssueDependencies(params) {
|
|
356
716
|
const now = isoNow();
|
|
357
717
|
this.connection
|
|
@@ -448,30 +808,13 @@ export class PatchRelayDatabase {
|
|
|
448
808
|
return Number(row?.count ?? 0);
|
|
449
809
|
}
|
|
450
810
|
listIssuesReadyForExecution() {
|
|
451
|
-
|
|
452
|
-
.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
SELECT 1
|
|
459
|
-
FROM issue_dependencies d
|
|
460
|
-
LEFT JOIN issues blockers
|
|
461
|
-
ON blockers.project_id = d.project_id
|
|
462
|
-
AND blockers.linear_issue_id = d.blocker_linear_issue_id
|
|
463
|
-
WHERE d.project_id = i.project_id
|
|
464
|
-
AND d.linear_issue_id = i.linear_issue_id
|
|
465
|
-
AND (
|
|
466
|
-
COALESCE(blockers.current_linear_state_type, d.blocker_current_linear_state_type, '') != 'completed'
|
|
467
|
-
AND LOWER(TRIM(COALESCE(blockers.current_linear_state, d.blocker_current_linear_state, ''))) != 'done'
|
|
468
|
-
)
|
|
469
|
-
)
|
|
470
|
-
`)
|
|
471
|
-
.all();
|
|
472
|
-
return rows.map((row) => ({
|
|
473
|
-
projectId: String(row.project_id),
|
|
474
|
-
linearIssueId: String(row.linear_issue_id),
|
|
811
|
+
return this.listIssues()
|
|
812
|
+
.filter((issue) => issue.activeRunId === undefined)
|
|
813
|
+
.filter((issue) => this.countUnresolvedBlockers(issue.projectId, issue.linearIssueId) === 0)
|
|
814
|
+
.filter((issue) => issue.pendingRunType !== undefined || this.peekIssueSessionWake(issue.projectId, issue.linearIssueId) !== undefined)
|
|
815
|
+
.map((issue) => ({
|
|
816
|
+
projectId: issue.projectId,
|
|
817
|
+
linearIssueId: issue.linearIssueId,
|
|
475
818
|
}));
|
|
476
819
|
}
|
|
477
820
|
/**
|
|
@@ -529,7 +872,12 @@ export class PatchRelayDatabase {
|
|
|
529
872
|
INSERT INTO runs (issue_id, project_id, linear_issue_id, run_type, status, prompt_text, started_at)
|
|
530
873
|
VALUES (?, ?, ?, ?, 'queued', ?, ?)
|
|
531
874
|
`).run(params.issueId, params.projectId, params.linearIssueId, params.runType, params.promptText ?? null, now);
|
|
532
|
-
|
|
875
|
+
const run = this.getRun(Number(result.lastInsertRowid));
|
|
876
|
+
const issue = this.getIssue(params.projectId, params.linearIssueId);
|
|
877
|
+
if (issue) {
|
|
878
|
+
this.syncIssueSessionFromIssue(issue, { lastRunType: run.runType });
|
|
879
|
+
}
|
|
880
|
+
return run;
|
|
533
881
|
}
|
|
534
882
|
getRun(id) {
|
|
535
883
|
const row = this.connection.prepare("SELECT * FROM runs WHERE id = ?").get(id);
|
|
@@ -571,7 +919,16 @@ export class PatchRelayDatabase {
|
|
|
571
919
|
turn_id = COALESCE(?, turn_id),
|
|
572
920
|
status = 'running'
|
|
573
921
|
WHERE id = ?
|
|
922
|
+
AND ended_at IS NULL
|
|
923
|
+
AND status IN ('queued', 'running')
|
|
574
924
|
`).run(params.threadId, params.parentThreadId ?? null, params.turnId ?? null, runId);
|
|
925
|
+
const run = this.getRun(runId);
|
|
926
|
+
if (!run)
|
|
927
|
+
return;
|
|
928
|
+
const issue = this.getIssue(run.projectId, run.linearIssueId);
|
|
929
|
+
if (issue) {
|
|
930
|
+
this.syncIssueSessionFromIssue(issue);
|
|
931
|
+
}
|
|
575
932
|
}
|
|
576
933
|
updateRunTurnId(runId, turnId) {
|
|
577
934
|
this.connection.prepare("UPDATE runs SET turn_id = ? WHERE id = ?").run(turnId, runId);
|
|
@@ -589,6 +946,16 @@ export class PatchRelayDatabase {
|
|
|
589
946
|
ended_at = ?
|
|
590
947
|
WHERE id = ?
|
|
591
948
|
`).run(params.status, params.threadId ?? null, params.turnId ?? null, params.failureReason ?? null, params.summaryJson ?? null, params.reportJson ?? null, now, runId);
|
|
949
|
+
const run = this.getRun(runId);
|
|
950
|
+
if (!run)
|
|
951
|
+
return;
|
|
952
|
+
const issue = this.getIssue(run.projectId, run.linearIssueId);
|
|
953
|
+
if (issue) {
|
|
954
|
+
this.syncIssueSessionFromIssue(issue, {
|
|
955
|
+
summaryText: extractLatestAssistantSummary(this.getRun(runId) ?? run),
|
|
956
|
+
lastRunType: run.runType,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
592
959
|
}
|
|
593
960
|
// ─── Thread Events (kept for extended history) ────────────────────
|
|
594
961
|
saveThreadEvent(params) {
|
|
@@ -613,9 +980,33 @@ export class PatchRelayDatabase {
|
|
|
613
980
|
}
|
|
614
981
|
// ─── View builders ──────────────────────────────────────────────
|
|
615
982
|
issueToTrackedIssue(issue) {
|
|
983
|
+
const session = this.getIssueSession(issue.projectId, issue.linearIssueId);
|
|
616
984
|
const blockedBy = this.listIssueDependencies(issue.projectId, issue.linearIssueId);
|
|
617
985
|
const unresolvedBlockedBy = blockedBy.filter((entry) => !isResolvedLinearState(entry.blockerCurrentLinearStateType, entry.blockerCurrentLinearState));
|
|
986
|
+
const pendingWake = this.peekIssueSessionWake(issue.projectId, issue.linearIssueId);
|
|
618
987
|
const failureContext = parseGitHubFailureContext(issue.lastGitHubFailureContextJson);
|
|
988
|
+
const blockedByKeys = unresolvedBlockedBy.map((entry) => entry.blockerIssueKey ?? entry.blockerLinearIssueId);
|
|
989
|
+
const waitingReason = derivePatchRelayWaitingReason({
|
|
990
|
+
...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
|
|
991
|
+
blockedByKeys,
|
|
992
|
+
factoryState: issue.factoryState,
|
|
993
|
+
pendingRunType: issue.pendingRunType,
|
|
994
|
+
prNumber: issue.prNumber,
|
|
995
|
+
prReviewState: issue.prReviewState,
|
|
996
|
+
prCheckStatus: issue.prCheckStatus,
|
|
997
|
+
latestFailureCheckName: issue.lastGitHubFailureCheckName,
|
|
998
|
+
});
|
|
999
|
+
const latestRun = this.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
1000
|
+
const latestEvent = this.listIssueSessionEvents(issue.projectId, issue.linearIssueId, { limit: 1 }).at(-1);
|
|
1001
|
+
const statusNote = deriveIssueStatusNote({
|
|
1002
|
+
issue,
|
|
1003
|
+
sessionSummary: session?.summaryText,
|
|
1004
|
+
latestRun,
|
|
1005
|
+
latestEvent,
|
|
1006
|
+
failureSummary: failureContext?.summary,
|
|
1007
|
+
blockedByKeys,
|
|
1008
|
+
waitingReason,
|
|
1009
|
+
});
|
|
619
1010
|
return {
|
|
620
1011
|
id: issue.id,
|
|
621
1012
|
projectId: issue.projectId,
|
|
@@ -623,17 +1014,31 @@ export class PatchRelayDatabase {
|
|
|
623
1014
|
...(issue.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
624
1015
|
...(issue.title ? { title: issue.title } : {}),
|
|
625
1016
|
...(issue.url ? { issueUrl: issue.url } : {}),
|
|
1017
|
+
...(statusNote ? { statusNote } : {}),
|
|
626
1018
|
...(issue.currentLinearState ? { currentLinearState: issue.currentLinearState } : {}),
|
|
1019
|
+
...(session?.sessionState ? { sessionState: session.sessionState } : {}),
|
|
627
1020
|
factoryState: issue.factoryState,
|
|
628
1021
|
blockedByCount: unresolvedBlockedBy.length,
|
|
629
|
-
blockedByKeys
|
|
630
|
-
|
|
631
|
-
|
|
1022
|
+
blockedByKeys,
|
|
1023
|
+
readyForExecution: isIssueSessionReadyForExecution({
|
|
1024
|
+
sessionState: session?.sessionState,
|
|
1025
|
+
factoryState: issue.factoryState,
|
|
1026
|
+
activeRunId: issue.activeRunId,
|
|
1027
|
+
blockedByCount: unresolvedBlockedBy.length,
|
|
1028
|
+
hasPendingWake: pendingWake !== undefined,
|
|
1029
|
+
hasLegacyPendingRun: issue.pendingRunType !== undefined,
|
|
1030
|
+
...(issue.prNumber !== undefined ? { prNumber: issue.prNumber } : {}),
|
|
1031
|
+
...(issue.prState ? { prState: issue.prState } : {}),
|
|
1032
|
+
...(issue.prReviewState ? { prReviewState: issue.prReviewState } : {}),
|
|
1033
|
+
...(issue.prCheckStatus ? { prCheckStatus: issue.prCheckStatus } : {}),
|
|
1034
|
+
...(issue.lastGitHubFailureSource ? { latestFailureSource: issue.lastGitHubFailureSource } : {}),
|
|
1035
|
+
}),
|
|
632
1036
|
...(issue.lastGitHubFailureSource ? { latestFailureSource: issue.lastGitHubFailureSource } : {}),
|
|
633
1037
|
...(issue.lastGitHubFailureHeadSha ? { latestFailureHeadSha: issue.lastGitHubFailureHeadSha } : {}),
|
|
634
1038
|
...(issue.lastGitHubFailureCheckName ? { latestFailureCheckName: issue.lastGitHubFailureCheckName } : {}),
|
|
635
1039
|
...(failureContext?.stepName ? { latestFailureStepName: failureContext.stepName } : {}),
|
|
636
1040
|
...(failureContext?.summary ? { latestFailureSummary: failureContext.summary } : {}),
|
|
1041
|
+
...(waitingReason ? { waitingReason } : {}),
|
|
637
1042
|
...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
|
|
638
1043
|
...(issue.agentSessionId ? { activeAgentSessionId: issue.agentSessionId } : {}),
|
|
639
1044
|
updatedAt: issue.updatedAt,
|
|
@@ -647,6 +1052,44 @@ export class PatchRelayDatabase {
|
|
|
647
1052
|
const issue = this.getIssueByKey(issueKey);
|
|
648
1053
|
return issue ? this.issueToTrackedIssue(issue) : undefined;
|
|
649
1054
|
}
|
|
1055
|
+
listIssues() {
|
|
1056
|
+
const rows = this.connection
|
|
1057
|
+
.prepare("SELECT * FROM issues ORDER BY updated_at DESC")
|
|
1058
|
+
.all();
|
|
1059
|
+
return rows.map(mapIssueRow);
|
|
1060
|
+
}
|
|
1061
|
+
listIssuesWithAgentSessions() {
|
|
1062
|
+
const rows = this.connection
|
|
1063
|
+
.prepare("SELECT * FROM issues WHERE agent_session_id IS NOT NULL ORDER BY updated_at DESC")
|
|
1064
|
+
.all();
|
|
1065
|
+
return rows.map(mapIssueRow);
|
|
1066
|
+
}
|
|
1067
|
+
findLatestAgentSessionIdForIssue(linearIssueId) {
|
|
1068
|
+
const row = this.connection.prepare(`
|
|
1069
|
+
SELECT COALESCE(
|
|
1070
|
+
json_extract(payload_json, '$.agentSession.id'),
|
|
1071
|
+
json_extract(payload_json, '$.data.agentSession.id'),
|
|
1072
|
+
json_extract(payload_json, '$.agentSessionId'),
|
|
1073
|
+
json_extract(payload_json, '$.data.agentSessionId')
|
|
1074
|
+
) AS agent_session_id
|
|
1075
|
+
FROM webhook_events
|
|
1076
|
+
WHERE COALESCE(
|
|
1077
|
+
json_extract(payload_json, '$.agentSession.issueId'),
|
|
1078
|
+
json_extract(payload_json, '$.data.agentSession.issueId'),
|
|
1079
|
+
json_extract(payload_json, '$.agentSession.issue.id'),
|
|
1080
|
+
json_extract(payload_json, '$.data.agentSession.issue.id')
|
|
1081
|
+
) = ?
|
|
1082
|
+
AND COALESCE(
|
|
1083
|
+
json_extract(payload_json, '$.agentSession.id'),
|
|
1084
|
+
json_extract(payload_json, '$.data.agentSession.id'),
|
|
1085
|
+
json_extract(payload_json, '$.agentSessionId'),
|
|
1086
|
+
json_extract(payload_json, '$.data.agentSessionId')
|
|
1087
|
+
) IS NOT NULL
|
|
1088
|
+
ORDER BY id DESC
|
|
1089
|
+
LIMIT 1
|
|
1090
|
+
`).get(linearIssueId);
|
|
1091
|
+
return row?.agent_session_id != null ? String(row.agent_session_id) : undefined;
|
|
1092
|
+
}
|
|
650
1093
|
// ─── Issue overview for query service ─────────────────────────────
|
|
651
1094
|
getIssueOverview(issueKey) {
|
|
652
1095
|
const issue = this.getIssueByKey(issueKey);
|
|
@@ -659,6 +1102,105 @@ export class PatchRelayDatabase {
|
|
|
659
1102
|
...(activeRun ? { activeRun } : {}),
|
|
660
1103
|
};
|
|
661
1104
|
}
|
|
1105
|
+
syncIssueSessionFromIssue(issue, options) {
|
|
1106
|
+
const tracked = this.issueToTrackedIssue(issue);
|
|
1107
|
+
const existing = this.getIssueSession(issue.projectId, issue.linearIssueId);
|
|
1108
|
+
const latestRun = this.getLatestRunForIssue(issue.projectId, issue.linearIssueId);
|
|
1109
|
+
const latestRunType = options?.lastRunType ?? latestRun?.runType ?? existing?.lastRunType;
|
|
1110
|
+
const summaryText = this.resolveIssueSessionSummary(issue, latestRun, existing?.summaryText, options?.summaryText);
|
|
1111
|
+
const activeThreadId = issue.threadId ?? existing?.activeThreadId;
|
|
1112
|
+
const threadGeneration = activeThreadId && activeThreadId !== existing?.activeThreadId
|
|
1113
|
+
? (existing?.threadGeneration ?? 0) + 1
|
|
1114
|
+
: (existing?.threadGeneration ?? (activeThreadId ? 1 : 0));
|
|
1115
|
+
const sessionState = deriveIssueSessionState({
|
|
1116
|
+
...(issue.activeRunId !== undefined ? { activeRunId: issue.activeRunId } : {}),
|
|
1117
|
+
factoryState: issue.factoryState,
|
|
1118
|
+
});
|
|
1119
|
+
const lastWakeReason = options?.lastWakeReason
|
|
1120
|
+
?? deriveIssueSessionWakeReason({
|
|
1121
|
+
pendingRunType: issue.pendingRunType,
|
|
1122
|
+
factoryState: issue.factoryState,
|
|
1123
|
+
prNumber: issue.prNumber,
|
|
1124
|
+
prState: issue.prState,
|
|
1125
|
+
prReviewState: issue.prReviewState,
|
|
1126
|
+
prCheckStatus: issue.prCheckStatus,
|
|
1127
|
+
latestFailureSource: issue.lastGitHubFailureSource,
|
|
1128
|
+
})
|
|
1129
|
+
?? existing?.lastWakeReason;
|
|
1130
|
+
const now = isoNow();
|
|
1131
|
+
if (existing) {
|
|
1132
|
+
this.connection.prepare(`
|
|
1133
|
+
UPDATE issue_sessions SET
|
|
1134
|
+
issue_key = ?,
|
|
1135
|
+
repo_id = ?,
|
|
1136
|
+
branch_name = ?,
|
|
1137
|
+
worktree_path = ?,
|
|
1138
|
+
pr_number = ?,
|
|
1139
|
+
pr_head_sha = ?,
|
|
1140
|
+
pr_author_login = ?,
|
|
1141
|
+
session_state = ?,
|
|
1142
|
+
waiting_reason = ?,
|
|
1143
|
+
summary_text = ?,
|
|
1144
|
+
active_thread_id = ?,
|
|
1145
|
+
thread_generation = ?,
|
|
1146
|
+
active_run_id = ?,
|
|
1147
|
+
last_run_type = ?,
|
|
1148
|
+
last_wake_reason = ?,
|
|
1149
|
+
ci_repair_attempts = ?,
|
|
1150
|
+
queue_repair_attempts = ?,
|
|
1151
|
+
review_fix_attempts = ?,
|
|
1152
|
+
updated_at = ?
|
|
1153
|
+
WHERE project_id = ? AND linear_issue_id = ?
|
|
1154
|
+
`).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);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
this.connection.prepare(`
|
|
1158
|
+
INSERT INTO issue_sessions (
|
|
1159
|
+
project_id, linear_issue_id, issue_key, repo_id, branch_name, worktree_path,
|
|
1160
|
+
pr_number, pr_head_sha, pr_author_login, session_state, waiting_reason, summary_text,
|
|
1161
|
+
active_thread_id, thread_generation, active_run_id, last_run_type, last_wake_reason,
|
|
1162
|
+
ci_repair_attempts, queue_repair_attempts, review_fix_attempts,
|
|
1163
|
+
created_at, updated_at
|
|
1164
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1165
|
+
`).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);
|
|
1166
|
+
}
|
|
1167
|
+
resolveIssueSessionSummary(issue, latestRun, existingSummaryText, explicitSummaryText) {
|
|
1168
|
+
if (explicitSummaryText?.trim()) {
|
|
1169
|
+
return explicitSummaryText;
|
|
1170
|
+
}
|
|
1171
|
+
const latestSummary = extractLatestAssistantSummary(latestRun);
|
|
1172
|
+
if (this.shouldKeepPreviousIssueSummary(issue, latestRun)) {
|
|
1173
|
+
return this.findLatestCompletedRunSummary(issue.projectId, issue.linearIssueId)
|
|
1174
|
+
?? existingSummaryText
|
|
1175
|
+
?? latestSummary;
|
|
1176
|
+
}
|
|
1177
|
+
return latestSummary ?? existingSummaryText;
|
|
1178
|
+
}
|
|
1179
|
+
shouldKeepPreviousIssueSummary(issue, latestRun) {
|
|
1180
|
+
if (!latestRun || latestRun.status !== "failed") {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
if (latestRun.summaryJson || latestRun.reportJson) {
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
return issue.factoryState === "pr_open"
|
|
1187
|
+
|| issue.factoryState === "awaiting_queue"
|
|
1188
|
+
|| issue.factoryState === "done";
|
|
1189
|
+
}
|
|
1190
|
+
findLatestCompletedRunSummary(projectId, linearIssueId) {
|
|
1191
|
+
const runs = this.listRunsForIssue(projectId, linearIssueId);
|
|
1192
|
+
for (let index = runs.length - 1; index >= 0; index -= 1) {
|
|
1193
|
+
const run = runs[index];
|
|
1194
|
+
if (!run || run.status !== "completed") {
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
const summary = extractLatestAssistantSummary(run);
|
|
1198
|
+
if (summary?.trim()) {
|
|
1199
|
+
return summary;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return undefined;
|
|
1203
|
+
}
|
|
662
1204
|
}
|
|
663
1205
|
// ─── Row mappers ──────────────────────────────────────────────────
|
|
664
1206
|
function mapIssueRow(row) {
|
|
@@ -680,18 +1222,23 @@ function mapIssueRow(row) {
|
|
|
680
1222
|
...(row.pending_run_type !== null && row.pending_run_type !== undefined ? { pendingRunType: String(row.pending_run_type) } : {}),
|
|
681
1223
|
...(row.pending_run_context_json !== null && row.pending_run_context_json !== undefined ? { pendingRunContextJson: String(row.pending_run_context_json) } : {}),
|
|
682
1224
|
...(row.branch_name !== null ? { branchName: String(row.branch_name) } : {}),
|
|
683
|
-
...(row.branch_owner !== null && row.branch_owner !== undefined
|
|
1225
|
+
...(row.branch_owner !== null && row.branch_owner !== undefined && String(row.branch_owner) === "patchrelay"
|
|
1226
|
+
? { branchOwner: "patchrelay" }
|
|
1227
|
+
: { branchOwner: "patchrelay" }),
|
|
684
1228
|
...(row.branch_ownership_changed_at !== null && row.branch_ownership_changed_at !== undefined
|
|
685
1229
|
? { branchOwnershipChangedAt: String(row.branch_ownership_changed_at) }
|
|
686
1230
|
: {}),
|
|
687
1231
|
...(row.worktree_path !== null ? { worktreePath: String(row.worktree_path) } : {}),
|
|
688
1232
|
...(row.thread_id !== null ? { threadId: String(row.thread_id) } : {}),
|
|
689
1233
|
...(row.active_run_id !== null ? { activeRunId: Number(row.active_run_id) } : {}),
|
|
1234
|
+
...(row.status_comment_id !== null && row.status_comment_id !== undefined ? { statusCommentId: String(row.status_comment_id) } : {}),
|
|
690
1235
|
...(row.agent_session_id !== null ? { agentSessionId: String(row.agent_session_id) } : {}),
|
|
691
1236
|
updatedAt: String(row.updated_at),
|
|
692
1237
|
...(row.pr_number !== null && row.pr_number !== undefined ? { prNumber: Number(row.pr_number) } : {}),
|
|
693
1238
|
...(row.pr_url !== null && row.pr_url !== undefined ? { prUrl: String(row.pr_url) } : {}),
|
|
694
1239
|
...(row.pr_state !== null && row.pr_state !== undefined ? { prState: String(row.pr_state) } : {}),
|
|
1240
|
+
...(row.pr_head_sha !== null && row.pr_head_sha !== undefined ? { prHeadSha: String(row.pr_head_sha) } : {}),
|
|
1241
|
+
...(row.pr_author_login !== null && row.pr_author_login !== undefined ? { prAuthorLogin: String(row.pr_author_login) } : {}),
|
|
695
1242
|
...(row.pr_review_state !== null && row.pr_review_state !== undefined ? { prReviewState: String(row.pr_review_state) } : {}),
|
|
696
1243
|
...(row.pr_check_status !== null && row.pr_check_status !== undefined ? { prCheckStatus: String(row.pr_check_status) } : {}),
|
|
697
1244
|
...(row.last_github_failure_source !== null && row.last_github_failure_source !== undefined
|
|
@@ -747,7 +1294,49 @@ function mapIssueRow(row) {
|
|
|
747
1294
|
reviewFixAttempts: Number(row.review_fix_attempts ?? 0),
|
|
748
1295
|
zombieRecoveryAttempts: Number(row.zombie_recovery_attempts ?? 0),
|
|
749
1296
|
...(row.last_zombie_recovery_at !== null && row.last_zombie_recovery_at !== undefined ? { lastZombieRecoveryAt: String(row.last_zombie_recovery_at) } : {}),
|
|
750
|
-
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
function mapIssueSessionRow(row) {
|
|
1300
|
+
return {
|
|
1301
|
+
id: Number(row.id),
|
|
1302
|
+
projectId: String(row.project_id),
|
|
1303
|
+
linearIssueId: String(row.linear_issue_id),
|
|
1304
|
+
...(row.issue_key !== null && row.issue_key !== undefined ? { issueKey: String(row.issue_key) } : {}),
|
|
1305
|
+
repoId: String(row.repo_id),
|
|
1306
|
+
...(row.branch_name !== null && row.branch_name !== undefined ? { branchName: String(row.branch_name) } : {}),
|
|
1307
|
+
...(row.worktree_path !== null && row.worktree_path !== undefined ? { worktreePath: String(row.worktree_path) } : {}),
|
|
1308
|
+
...(row.pr_number !== null && row.pr_number !== undefined ? { prNumber: Number(row.pr_number) } : {}),
|
|
1309
|
+
...(row.pr_head_sha !== null && row.pr_head_sha !== undefined ? { prHeadSha: String(row.pr_head_sha) } : {}),
|
|
1310
|
+
...(row.pr_author_login !== null && row.pr_author_login !== undefined ? { prAuthorLogin: String(row.pr_author_login) } : {}),
|
|
1311
|
+
sessionState: String(row.session_state),
|
|
1312
|
+
...(row.waiting_reason !== null && row.waiting_reason !== undefined ? { waitingReason: String(row.waiting_reason) } : {}),
|
|
1313
|
+
...(row.summary_text !== null && row.summary_text !== undefined ? { summaryText: String(row.summary_text) } : {}),
|
|
1314
|
+
...(row.active_thread_id !== null && row.active_thread_id !== undefined ? { activeThreadId: String(row.active_thread_id) } : {}),
|
|
1315
|
+
threadGeneration: Number(row.thread_generation ?? 0),
|
|
1316
|
+
...(row.active_run_id !== null && row.active_run_id !== undefined ? { activeRunId: Number(row.active_run_id) } : {}),
|
|
1317
|
+
...(row.last_run_type !== null && row.last_run_type !== undefined ? { lastRunType: String(row.last_run_type) } : {}),
|
|
1318
|
+
...(row.last_wake_reason !== null && row.last_wake_reason !== undefined ? { lastWakeReason: String(row.last_wake_reason) } : {}),
|
|
1319
|
+
ciRepairAttempts: Number(row.ci_repair_attempts ?? 0),
|
|
1320
|
+
queueRepairAttempts: Number(row.queue_repair_attempts ?? 0),
|
|
1321
|
+
reviewFixAttempts: Number(row.review_fix_attempts ?? 0),
|
|
1322
|
+
...(row.lease_id !== null && row.lease_id !== undefined ? { leaseId: String(row.lease_id) } : {}),
|
|
1323
|
+
...(row.worker_id !== null && row.worker_id !== undefined ? { workerId: String(row.worker_id) } : {}),
|
|
1324
|
+
...(row.leased_until !== null && row.leased_until !== undefined ? { leasedUntil: String(row.leased_until) } : {}),
|
|
1325
|
+
createdAt: String(row.created_at),
|
|
1326
|
+
updatedAt: String(row.updated_at),
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
function mapIssueSessionEventRow(row) {
|
|
1330
|
+
return {
|
|
1331
|
+
id: Number(row.id),
|
|
1332
|
+
projectId: String(row.project_id),
|
|
1333
|
+
linearIssueId: String(row.linear_issue_id),
|
|
1334
|
+
eventType: String(row.event_type),
|
|
1335
|
+
...(row.event_json !== null && row.event_json !== undefined ? { eventJson: String(row.event_json) } : {}),
|
|
1336
|
+
...(row.dedupe_key !== null && row.dedupe_key !== undefined ? { dedupeKey: String(row.dedupe_key) } : {}),
|
|
1337
|
+
createdAt: String(row.created_at),
|
|
1338
|
+
...(row.processed_at !== null && row.processed_at !== undefined ? { processedAt: String(row.processed_at) } : {}),
|
|
1339
|
+
...(row.consumed_by_run_id !== null && row.consumed_by_run_id !== undefined ? { consumedByRunId: Number(row.consumed_by_run_id) } : {}),
|
|
751
1340
|
};
|
|
752
1341
|
}
|
|
753
1342
|
function mapRunRow(row) {
|